Skip to content

Commit

Permalink
Process tweets and sent to Discord
Browse files Browse the repository at this point in the history
  • Loading branch information
MattIPv4 committed Apr 11, 2021
1 parent 80c876f commit 9c6ead6
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 16 deletions.
2 changes: 2 additions & 0 deletions development.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_DSN=
TWITTER_BEARER_AUTH=
TWITTER_USER_ID=
DISCORD_WEBHOOK=
2 changes: 2 additions & 0 deletions production.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_DSN=
TWITTER_BEARER_AUTH=
TWITTER_USER_ID=
DISCORD_WEBHOOK=
112 changes: 99 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,102 @@ const textResponse = content => new Response(content, {
},
});

// Util to send a JSON response
const jsonResponse = obj => new Response(JSON.stringify(obj), {
headers: {
'Content-Type': 'application/json',
},
// Util to suppress for Discord
const suppressLinks = text => text.replace(/(https?:\/\/\S+)/g, '<$1>')

// Util to escape Discord markdown in text
const escapeMarkdown = text => suppressLinks(text)
.replace(/\\([*_`~\\])/g, '$1') // unescape already escaped chars
.replace(/([*_`~\\])/g, '\\$1'); // escape all MD chars

// Util to add quote markdown to text
const quoteText = text => `> ${text.split('\n').join('\n> ')}`;

// Fetch latest Tweet data from Twitter
const fetchLatestTweets = since => {
// Define the query params for the data we need
const req = new URL(`https://api.twitter.com/2/users/${process.env.TWITTER_USER_ID}/tweets`);
req.searchParams.set('max_results', '100');
req.searchParams.set('tweet.fields', ['referenced_tweets', 'text', 'created_at', 'id', 'author_id'].join(','));
req.searchParams.set('user.fields', ['username', 'profile_image_url'].join(','));
req.searchParams.set('expansions', ['author_id', 'referenced_tweets.id', 'referenced_tweets.id.author_id'].join(','));
if (since) req.searchParams.set('since_id', since);

// Make the request with the OAuth 2 token
return fetch(req.toString(), { headers: { Authorization: `Bearer ${process.env.TWITTER_BEARER_AUTH}` } })
.then(req => req.json());
};

// Post tweet information to Discord
const postDiscordTweet = (type, content, links, username, avatar) => fetch(process.env.DISCORD_WEBHOOK, {
method: 'POST',
headers: { 'Content-type': 'application/json' },
body: JSON.stringify({
content: `**${type}**\n\n${quoteText(escapeMarkdown(content))}\n\n${suppressLinks(links)}`,
username,
avatar_url: avatar,
}),
});

const fetchRecentTweets = lastId => fetch(
`https://api.twitter.com/2/users/:id/tweets?limit=100${lastId ? `&since=${lastId}` : ''}`,
{ headers: { Authorization: `Bearer ${process.env.TWITTER_BEARER_AUTH}` } },
).then(req => req.json());
// Process a raw Tweet from Discord and send it to Discord
const processTweet = (tweet, includes) => {
// Resolve the author of the tweet
const author = includes.users.find(inclUser => inclUser.id === tweet.author_id);

// Resolve any referenced tweet
const refTweet = tweet.referenced_tweets && tweet.referenced_tweets.length
? includes.tweets.find(inclTweet => inclTweet.id === tweet.referenced_tweets[0].id)
: null;
const refType = refTweet ? tweet.referenced_tweets[0].type : null;
const refAuthor = refTweet ? includes.users.find(inclUser => inclUser.id === refTweet.author_id) : null;

// Determine the Discord title
const title = refType === 'retweeted'
? '🔁 Retweeted'
: refType === 'quoted'
? '📝 Quoted'
: refType === 'replied_to'
? '⤴️ Replied'
: '💬 Tweeted';

// Determine what content to use
const content = refType === 'retweeted' ? refTweet.text : tweet.text;

// Determine what links to reference
const links = refType === 'retweeted'
? `https://twitter.com/${refAuthor.username}/status/${refTweet.id}`
: refType === 'quoted'
? `https://twitter.com/${author.username}/status/${tweet.id}\nQuoting https://twitter.com/${refAuthor.username}/status/${refTweet.id}`
: refType === 'replied_to'
? `https://twitter.com/${author.username}/status/${tweet.id}\nReplying to https://twitter.com/${refAuthor.username}/status/${refTweet.id}`
: `https://twitter.com/${author.username}/status/${tweet.id}`;

// Post to Discord
return postDiscordTweet(title, content, links, author.username, author.profile_image_url);
};

// Mirror latest tweets from Twitter to Discord
const mirrorLatestTweets = async () => {
const data = await fetchRecentTweets();
console.log(data);
// Get the last tweet we processed
const last = await TWEETS_TO_DISCORD_LAST_TWEET.get('latest_id');

// Get new tweets since last processed (oldest first)
const data = await fetchLatestTweets(last);
const tweets = data.data ? data.data.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) : [];

// If we haven't run before, store the most recent tweet and abort
if (!last && tweets.length) {
await TWEETS_TO_DISCORD_LAST_TWEET.put('latest_id', tweets[tweets.length - 1].id);
return;
}

// Process each tweet
for (const tweet of tweets) {
console.log(await processTweet(tweet, data.includes).then(res => res.text()));

// Store this as the most recent tweet we've processed
await TWEETS_TO_DISCORD_LAST_TWEET.put('latest_id', tweet.id);
}
};

// Process all requests to the worker
Expand All @@ -37,8 +118,13 @@ const handleRequest = async ({ request, wait, sentry }) => {
// Execute triggers route
if (url.pathname === '/execute') {
// Trigger each workflow in the background after
wait(mirrorLatestTweets().catch(err => sentry.captureException(err)));
return jsonResponse({});
wait(mirrorLatestTweets().catch(err => {
// Log & re-throw any errors
console.error(err);
sentry.captureException(err);
throw err;
}));
return textResponse('Executed');
}

// Not found
Expand Down
6 changes: 3 additions & 3 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ account_id = "fb1f542488f2441acf88ca15f3a8390d"
workers_dev = true
webpack_config = "webpack.config.js"
kv_namespaces = [
{ binding = "TWEETS_TO_DISCORD_LAST_TWEET", id = "4fc9f04e2f29425c878cf74200359002", preview_id = "4fc9f04e2f29425c878cf74200359002" }
{ binding = "TWEETS_TO_DISCORD_LAST_TWEET", id = "f22f1aee1f3642bcaa81eaf306528577", preview_id = "f22f1aee1f3642bcaa81eaf306528577" }
]

[triggers]
crons = ["* * * * *"]
crons = ["*/2 * * * *"]

[env.production]
kv_namespaces = [
{ binding = "TWEETS_TO_DISCORD_LAST_TWEET", id = "e8a41596dbff45ab84f7833da8834bbb" }
{ binding = "TWEETS_TO_DISCORD_LAST_TWEET", id = "87d5c2be0d2342f680bec6a0b20a372d" }
]

0 comments on commit 9c6ead6

Please sign in to comment.