123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- import { z } from "https://deno.land/x/zod@v3.8.0/mod.ts";
- import { TokenProvider } from "./token.ts";
- const TWITTER_API = 'https://api.twitter.com';
- function object<T extends z.ZodRawShape>(shape: T): z.ZodObject<T, "passthrough"> {
- return z.object(shape).passthrough();
- }
- const IndicesSchema = z.tuple([ z.number(), z.number() ]);
- const BaseUserSchema = object({
- id_str: z.string(),
- name: z.string(),
- screen_name: z.string(),
- });
- const UserSchema = BaseUserSchema.extend({
- profile_image_url_https: z.string(),
- });
- export type User = z.infer<typeof UserSchema>;
- const HashtagSchema = object({
- text: z.string(),
- indices: IndicesSchema,
- });
- const UrlSchema = object({
- display_url: z.string(),
- expanded_url: z.string(),
- url: z.string(),
- indices: IndicesSchema,
- });
- const BaseTweetSchema = object({
- id_str: z.string(),
- created_at: z.string(),
- full_text: z.string(),
- user: UserSchema,
- display_text_range: IndicesSchema,
- lang: z.optional(z.string()),
- entities: object({
- hashtags: z.array(HashtagSchema),
- urls: z.array(UrlSchema),
- user_mentions: z.array(BaseUserSchema.extend({
- indices: IndicesSchema,
- })),
- }),
- is_quote_status: z.boolean(),
- extended_entities: z.optional(object({
- media: z.array(object({
- display_url: z.string(),
- media_url_https: z.string(),
- expanded_url: z.string(),
- indices: IndicesSchema,
- type: z.enum(['photo', 'video', 'animated_gif']),
- video_info: z.optional(object({
- variants: z.array(object({
- bitrate: z.optional(z.number()),
- content_type: z.string(),
- url: z.string(),
- })),
- })),
- })),
- })),
- in_reply_to_user_id_str: z.union([z.string(), z.null()]),
- });
- interface TweetRecurse {
- quoted_status?: Tweet,
- retweeted_status?: Tweet,
- }
- export type Tweet = z.infer<typeof BaseTweetSchema> & TweetRecurse;
- export const TweetSchema: z.ZodSchema<Tweet> = z.lazy(() => BaseTweetSchema.extend({
- quoted_status: z.optional(TweetSchema),
- retweeted_status: z.optional(TweetSchema),
- }));
- export const TimelineSchema = z.array(TweetSchema);
- export class TwitterClient {
- constructor(private readonly tokenProvider: TokenProvider) {}
- async getTimeline(username: string, newerThanId?: string): Promise<Tweet[]> {
- const options: Record<string, string> = {
- screen_name: username,
- trim_user: 'false',
- include_rts: 'true',
- tweet_mode: 'extended',
- };
- if (newerThanId !== undefined) {
- options.since_id = newerThanId;
- options.count = '100';
- }
- const result = await this.twitterGet(`/1.1/statuses/user_timeline.json`, options);
- return TimelineSchema.parse(result);
- }
- private async twitterGet(path: string, params: Record<string, string> = {}): Promise<unknown> {
- const url = new URL(`${TWITTER_API}${path}`);
- url.search = new URLSearchParams(params).toString();
- const result = await fetch(url, {
- headers: {
- Authorization: `Bearer ${await this.tokenProvider.getToken()}`
- }
- });
- if (result.status < 200 || result.status >= 300) {
- throw new Error(await result.text());
- }
- return result.json();
- }
- }
|