import OAuth from "oauth"; import "dotenv/config"; import { setTimeout } from "timers/promises"; import Koa from "koa"; const { API_KEY, API_SECRET, BEARER_TOKEN, ACCESS_TOKEN, ACCESS_TOKEN_SECRET } = process.env; function getAuthHeader(oauth: OAuth.OAuth, url: string) { return oauth.authHeader(url, ACCESS_TOKEN as string, ACCESS_TOKEN_SECRET as string, "post"); } const client = new OAuth.OAuth( "https://api.twitter.com/oauth/request_token", "https://api.twitter.com/oauth/access_token", API_KEY as string, API_SECRET as string, "1.0A", null, "HMAC-SHA1" ); const BOT_ID = "1612094318906417152"; interface TweetMentions { data: Array<{ id: string; text: string }>; meta: { result_count: number; newest_id: string; oldest_id: string; }; } interface TweetLookups { data: Array<{ id: string; conversation_id: "string"; text: string; created_at: string; attachments?: { media_keys: string[] }; }>; includes: { media: Array<{ media_key: string; url: string }> }; } async function ff(url: string) { const resp = await fetch(`https://api.twitter.com/2/${url}`, { headers: { Authorization: `Bearer ${BEARER_TOKEN}` }, }); if (resp.status !== 200) { throw new Error("invalid status: " + resp.status + "- " + (await resp.text())); } return await resp.json(); } let lastCreatedAt = ""; let lastMentionedTweet = ""; async function lookupTweets() { const data: TweetMentions = await ff( `users/${BOT_ID}/mentions?${ lastCreatedAt && `start_time=${new Date(new Date(lastCreatedAt).getTime() + 1).toJSON()}` }` ); if (!data.data || !data.data.length) { return; } let lookups: TweetLookups = await ff( `tweets?ids=${data.data .map((t) => t.id) .join( "," )}&tweet.fields=created_at&expansions=attachments.media_keys&media.fields=duration_ms,height,media_key,preview_image_url,public_metrics,type,url,width,alt_text` ); if (!lastCreatedAt) { console.log("added mention", lookups.data[0].created_at, lookups.data[0].id); lastCreatedAt = lookups.data[0].created_at; lastMentionedTweet = lookups.data[0].id; return lookupTweets(); } const tweets = lookups.data.filter( (tweet) => tweet.attachments?.media_keys.length === 1 && tweet.id > lastMentionedTweet ); if (tweets.length) { lastCreatedAt = lookups.data[0].created_at; lastMentionedTweet = lookups.data[0].id; } console.log(lastCreatedAt); for (const tweet of tweets) { const imageUrl = lookups.includes.media.find((media) => media.media_key === tweet?.attachments!.media_keys[0]) ?.url!; console.log("imageUrl", imageUrl); const imageResp = await fetch(imageUrl); const contentType = imageResp.headers.get("Content-Type"); const image = await imageResp.arrayBuffer(); console.log(contentType, image); const altText = await fetch("https://olivierdehaene-git-large-coco.hf.space/run/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: [`data:${contentType};base64,${Buffer.from(image).toString("base64")}`], }), }) .then((r) => r.json()) .then((r) => r.data); console.log(altText[0]); const header = getAuthHeader(client, "https://api.twitter.com/2/tweets"); const r = await fetch("https://api.twitter.com/2/tweets", { headers: { Authorization: header, "user-agent": "v3CreateTweetJS", "content-type": "application/json", accept: "application/json", }, body: JSON.stringify({ text: altText[0], reply: { in_reply_to_tweet_id: tweet!.id, conversation_id: tweet!.conversation_id }, }), method: "post", }); try { console.log("end", await r.json()); } catch {} } } process.on("unhandledRejection", async (err) => { console.error("unhandled rejection", err); }); process.on("uncaughtException", console.error); async function run() { while (1) { console.log("looking up"); try { await lookupTweets(); } catch (err) { console.error(err); } await setTimeout(5_000); } } run(); const SPACE_ID = process.env.SPACE_ID || "huggingface-projects/twitter-alt-image-bot"; // Keep space alive // setInterval(() => { // fetch(`https://${SPACE_ID.replace("/", "-")}.hf.space`); // }, 24 * 3600 * 1000); const app = new Koa(); app.use((ctx) => { ctx.body = "Check out the README!"; }); app.listen(7860);