index.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { Tweet, TwitterClient } from './twitter.ts';
  2. import { SqliteStore, Store } from './store.ts';
  3. import { StaticTokenProvider } from './token.ts';
  4. import { timelineAsHTML, timelineAsJSON } from './format.ts';
  5. import { z } from "https://deno.land/x/zod@v3.8.0/mod.ts";
  6. const MAX_INT = "9223372036854775807";
  7. class Server {
  8. constructor(
  9. readonly store: Store,
  10. readonly client: TwitterClient,
  11. ) {}
  12. async getLatestTweets(username: string, interval: number = 600): Promise<Tweet[]> {
  13. const storedAuthorId = this.store.getAuthorId(username);
  14. const latestTweet = storedAuthorId === null ? null : this.store.getLatestTweet(storedAuthorId);
  15. let timeline = await this.client.getTimeline(username, latestTweet?.id_str);
  16. console.log(`Fetched ${timeline.length} new tweets`);
  17. for (const tweet of timeline) {
  18. this.store.saveTweet(tweet);
  19. }
  20. const newerThan = new Date();
  21. newerThan.setSeconds(newerThan.getSeconds() - interval);
  22. if (timeline.length === 0) {
  23. if (storedAuthorId === null) {
  24. return [];
  25. } else {
  26. timeline = this.store.getLatestTweets(storedAuthorId, MAX_INT, newerThan);
  27. }
  28. } else {
  29. const lastTweet = timeline[timeline.length - 1];
  30. const olderTweets = this.store.getLatestTweets(lastTweet.user.id_str, lastTweet.id_str, newerThan);
  31. timeline = timeline.concat(olderTweets);
  32. }
  33. return timeline;
  34. }
  35. async start() {
  36. const server = Deno.listen({ port: 8000 });
  37. console.log("Server running at http://localhost:8000");
  38. for await (const conn of server) {
  39. this.serveHttp(conn);
  40. }
  41. }
  42. async serveHttp(conn: Deno.Conn) {
  43. const httpConn = Deno.serveHttp(conn);
  44. for await (const requestEvent of httpConn) {
  45. this.handle(requestEvent)
  46. .then(response => requestEvent.respondWith(response))
  47. .catch(err => console.error(err));
  48. }
  49. }
  50. async handle(requestEvent: Deno.RequestEvent): Promise<Response> {
  51. const url = new URL(requestEvent.request.url);
  52. const path = url.pathname;
  53. if (path === '/__proxy') {
  54. const target = url.searchParams.get('target');
  55. return this.handleProxy(requestEvent.request.headers, target);
  56. } else if (path.startsWith('/timeline/')) {
  57. const username = path.substring(10);
  58. return this.handleTimeline(username);
  59. } else if (path.startsWith('/json/')) {
  60. const username = path.substring(6);
  61. return this.handleJson(username);
  62. } else {
  63. return new Response('Not found', {
  64. status: 404,
  65. });
  66. }
  67. }
  68. async handleTimeline(username: string): Promise<Response> {
  69. const tweets = await this.getLatestTweets(username, 24 * 60 * 60);
  70. console.time('renderHTML');
  71. const html = timelineAsHTML(tweets);
  72. console.timeEnd('renderHTML');
  73. return new Response(html, {
  74. status: 200,
  75. headers: {
  76. 'Content-Type': 'text/html; charset=utf-8'
  77. }
  78. });
  79. }
  80. async handleJson(username: string): Promise<Response> {
  81. const tweets = await this.getLatestTweets(username, 24 * 60 * 60);
  82. console.time('renderJSON');
  83. const html = timelineAsJSON(username, tweets);
  84. console.timeEnd('renderJSON');
  85. return new Response(html, {
  86. status: 200,
  87. headers: {
  88. 'Content-Type': 'application/feed+json; charset=utf-8'
  89. }
  90. });
  91. }
  92. async handleProxy(headers: Headers, target: string | null): Promise<Response> {
  93. if (target !== null) {
  94. console.log(`Proxying ${target}`);
  95. try {
  96. const proxyRequest = await fetch(target, {
  97. headers: headers,
  98. });
  99. return new Response(proxyRequest.body, {
  100. status: proxyRequest.status,
  101. headers: proxyRequest.headers,
  102. });
  103. } catch (e) {
  104. return new Response('Bad request', {
  105. status: 400,
  106. });
  107. }
  108. } else {
  109. return new Response('Not found', {
  110. status: 404,
  111. });
  112. }
  113. }
  114. }
  115. const ConfigSchema = z.strictObject({
  116. bearer: z.string(),
  117. store: z.string(),
  118. });
  119. const configFile = Deno.args.length === 0 ? 'config.json' : Deno.args[0];
  120. const json = JSON.parse(Deno.readTextFileSync(configFile));
  121. const config = ConfigSchema.parse(json);
  122. const fetcher = new Server(
  123. new SqliteStore(config.store),
  124. new TwitterClient(new StaticTokenProvider(config.bearer)),
  125. );
  126. fetcher.start();