123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- import { Tweet, TwitterClient } from './twitter.ts';
- import { SqliteStore, Store } from './store.ts';
- import { StaticTokenProvider } from './token.ts';
- import { timelineAsHTML, timelineAsJSON } from './format.ts';
- import { z } from "https://deno.land/x/zod@v3.8.0/mod.ts";
- const MAX_INT = "9223372036854775807";
- class Server {
- constructor(
- readonly store: Store,
- readonly client: TwitterClient,
- ) {}
- async getLatestTweets(username: string, interval: number = 600): Promise<Tweet[]> {
- const storedAuthorId = this.store.getAuthorId(username);
- const latestTweet = storedAuthorId === null ? null : this.store.getLatestTweet(storedAuthorId);
- let authorId = storedAuthorId;
- let timeline = await this.client.getTimeline(username, latestTweet?.id_str);
- console.log(`Fetched ${timeline.length} new tweets`);
- for (const tweet of timeline) {
- this.store.saveTweet(tweet);
- }
- const newerThan = new Date();
- newerThan.setSeconds(newerThan.getSeconds() - interval);
- if (timeline.length === 0) {
- if (storedAuthorId === null) {
- return [];
- } else {
- timeline = this.store.getLatestTweets(storedAuthorId, MAX_INT, newerThan);
- }
- } else {
- const lastTweet = timeline[timeline.length - 1];
- const olderTweets = this.store.getLatestTweets(lastTweet.user.id_str, lastTweet.id_str, newerThan);
- timeline = timeline.concat(olderTweets);
- authorId = lastTweet.user.id_str;
- }
- timeline = timeline.filter(({ in_reply_to_user_id_str }) => {
- return in_reply_to_user_id_str === null || in_reply_to_user_id_str === authorId;
- });
- return timeline;
- }
- async start() {
- const server = Deno.listen({ port: 8000 });
- console.log("Server running at http://localhost:8000");
- for await (const conn of server) {
- this.serveHttp(conn);
- }
- }
- async serveHttp(conn: Deno.Conn) {
- const httpConn = Deno.serveHttp(conn);
- for await (const requestEvent of httpConn) {
- this.handle(requestEvent)
- .then(response => requestEvent.respondWith(response))
- .catch(err => console.error(err));
- }
- }
- async handle(requestEvent: Deno.RequestEvent): Promise<Response> {
- const url = new URL(requestEvent.request.url);
- const path = url.pathname;
- if (path === '/__proxy') {
- const target = url.searchParams.get('target');
- return this.handleProxy(requestEvent.request.headers, target);
- } else if (path.startsWith('/timeline/')) {
- const username = path.substring(10);
- return this.handleTimeline(username);
- } else if (path.startsWith('/json/')) {
- const username = path.substring(6);
- return this.handleJson(username);
- } else {
- return new Response('Not found', {
- status: 404,
- });
- }
- }
- async handleTimeline(username: string): Promise<Response> {
- const tweets = await this.getLatestTweets(username, 24 * 60 * 60);
- console.time('renderHTML');
- const html = timelineAsHTML(tweets);
- console.timeEnd('renderHTML');
- return new Response(html, {
- status: 200,
- headers: {
- 'Content-Type': 'text/html; charset=utf-8'
- }
- });
- }
- async handleJson(username: string): Promise<Response> {
- const tweets = await this.getLatestTweets(username, 24 * 60 * 60);
- console.time('renderJSON');
- const html = timelineAsJSON(username, tweets);
- console.timeEnd('renderJSON');
- return new Response(html, {
- status: 200,
- headers: {
- 'Content-Type': 'application/feed+json; charset=utf-8'
- }
- });
- }
- async handleProxy(headers: Headers, target: string | null): Promise<Response> {
- if (target !== null) {
- console.log(`Proxying ${target}`);
- try {
- const proxyRequest = await fetch(target, {
- headers: headers,
- });
- return new Response(proxyRequest.body, {
- status: proxyRequest.status,
- headers: proxyRequest.headers,
- });
- } catch (e) {
- return new Response('Bad request', {
- status: 400,
- });
- }
- } else {
- return new Response('Not found', {
- status: 404,
- });
- }
- }
- }
- const ConfigSchema = z.strictObject({
- bearer: z.string(),
- store: z.string(),
- });
- const configFile = Deno.args.length === 0 ? 'config.json' : Deno.args[0];
- const json = JSON.parse(Deno.readTextFileSync(configFile));
- const config = ConfigSchema.parse(json);
- const fetcher = new Server(
- new SqliteStore(config.store),
- new TwitterClient(new StaticTokenProvider(config.bearer)),
- );
- fetcher.start();
|