Last active
June 20, 2022 05:58
-
-
Save flaki/d53fb0e83ebbf3ca86c8fe4bb52ad0fa to your computer and use it in GitHub Desktop.
Creating a Twitter-Mastodon crossposter in WebAssembly with Atmo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// DON'T FORGET to update the domain name below to your instance's host! | |
const MASTODON_INSTANCE = 'mastodon.example'; | |
// Add your access token in here | |
const MASTODON_ACCESS_TOKEN = ''; | |
// We want to use the HTTP client API | |
import { http } from "@suborbital/runnable"; | |
// The runtime will invoke the exported "run" function to run our logic | |
export const run = (input) => { | |
// The message we want to send in the status update | |
// We will just hardcode this for now, and get it from Twitter in a later step! | |
let message = `Hello from WebAssembly!`; | |
// The Mastodon API endpoint we need to call to post a new status update | |
let url = `https://${MASTODON_INSTANCE}/api/v1/statuses`; | |
// We configure some headers to authenticate our request and set the request type to that expected by the Mastodon API | |
let headers = { | |
'Authorization': 'Bearer '+MASTODON_ACCESS_TOKEN, | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}; | |
// This Mastodon API uses classic "HTTP form" encoding (and not e.g. JSON) so we encode our POST data in the correct format | |
let body = 'status='+encodeURIComponent(message); | |
// Toot away! | |
http.post(url, body, headers); | |
// The result of our function | |
return 'ok'; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// DON'T FORGET to update the domain name below to your instance's host! | |
const MASTODON_INSTANCE = 'mastodon.example'; | |
// Add your access token in here | |
const MASTODON_ACCESS_TOKEN = ''; | |
// Configure the Twitter API Bearer Token here | |
const TWITTER_BEARER_TOKEN = ''; | |
// Choose the @username you want to cross-post tweets from | |
const TWITTER_USERNAME = 'SuborbitalDev'; | |
// We want to use the HTTP client API and the logging API to show some debug output | |
import { http, log } from "@suborbital/runnable"; | |
// The runtime will invoke the exported "run" function to run our logic | |
export const run = (input) => { | |
// The request returns a HttpResponse object with various data format methods | |
let idRequest = http.get('https://api.twitter.com/2/users/by/username/SuborbitalDev', { | |
'Authorization': 'Bearer '+TWITTER_BEARER_TOKEN | |
}); | |
// We can get the result back as .json(), .text(), or even an .arrayBuffer() | |
let idResult = idRequest.json(); | |
let twitterId = idResult.data.id; | |
// We will log this into the console for debugging purposes! | |
log.info(`@${TWITTER_USERNAME} resolved to the Twitter ID: ${twitterId}`); | |
// We can now use the twitterId to request the user's timeline and fetch some tweets! | |
let tweetRequest = http.get(`https://api.twitter.com/2/users/${twitterId}/tweets`, { | |
'Authorization': 'Bearer '+TWITTER_BEARER_TOKEN | |
}); | |
// .data of the returned JSON has a list (array) of the most recent tweets | |
let recentTweets = tweetRequest.json().data; | |
// For now, we just take the first of the lot and will improve on this in the next step | |
let tweet = recentTweets[0]; | |
// We now have a "message" content that came straight from Twitter! | |
let message = tweet.text; | |
// The Mastodon API endpoint we need to call to post a new status update | |
let url = `https://${MASTODON_INSTANCE}/api/v1/statuses`; | |
// We configure some headers to authenticate our request and set the request type to that expected by the Mastodon API | |
let headers = { | |
'Authorization': 'Bearer '+MASTODON_ACCESS_TOKEN, | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}; | |
// This Mastodon API uses classic "HTTP form" encoding (and not e.g. JSON) so we encode our POST data in the correct format | |
let body = 'status='+encodeURIComponent(message); | |
// Toot away! | |
http.post(url, body, headers); | |
// The result of our function | |
return 'ok'; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// DON'T FORGET to update the domain name below to your instance's host! | |
const MASTODON_INSTANCE = 'mastodon.example'; | |
// Add your access token in here | |
const MASTODON_ACCESS_TOKEN = ''; | |
// Configure the Twitter API Bearer Token here | |
const TWITTER_BEARER_TOKEN = ''; | |
// Choose the @username you want to cross-post tweets from | |
const TWITTER_USERNAME = 'SuborbitalDev'; | |
// New addition besides http & log APIs is the cache API | |
import { http, log, cache } from "@suborbital/runnable"; | |
// The runtime will invoke the exported "run" function to run our logic | |
export const run = (input) => { | |
// Headers we will use to authenticate our requests to the Twitter API | |
let twitterAuthHeaders = { | |
'Authorization': 'Bearer '+TWITTER_BEARER_TOKEN | |
}; | |
// We check the cache API for the cached twitterId, if not found this will throw | |
let twitterId; | |
try { | |
twitterId = cache.get('TWITTER_ID'); | |
} catch(e) { | |
// Not cached yet, we fall back to getting the ID from the API | |
log.info("Twitter ID not yet cached."); | |
let idRequest = http.get('https://api.twitter.com/2/users/by/username/'+TWITTER_USERNAME, twitterAuthHeaders); | |
let idResult = idRequest.json(); | |
// Note the missing declaration, this is updates the variable in the outer scope | |
twitterId = idResult.data.id; | |
// We store the resolved ID in the cache for the next run | |
// The last parameter (0) means we do not want the item to expire (TTL) | |
cache.set('TWITTER_ID', twitterId, 0); | |
} | |
log.info(`Using ${twitterId} for @${TWITTER_USERNAME}`); | |
// We can now use the twitterId to request the user's timeline and fetch some tweets! | |
let tweetsApiUrl = `https://api.twitter.com/2/users/${twitterId}/tweets` | |
// We check the cache for a last-posted tweet ID and take it into account if we find one | |
let tweet; | |
try { | |
let lastTweetId = cache.get('LAST_TWEET_ID'); | |
// We use since_id to only get newer tweets (which may be none!) | |
tweetsApiUrl += '?since_id='+lastTweetId | |
// Send the modified request | |
let newTweets = http.get(tweetsApiUrl, twitterAuthHeaders).json(); | |
// Tweets are returned in reverse-chronological order (newest first) | |
// We want to cross-post the *oldest* here first, which is the "next" tweet | |
// This is so later runs of our function send out all the rest, one by one | |
// Note: .data will be unset (undefined) if there are no more tweets | |
tweet = newTweets.data ? newTweets.data.pop() : undefined; | |
} catch(e) { | |
// This is running for the first time, so we list the tweets and get the latest | |
let recentTweets = http.get(tweetsApiUrl, twitterAuthHeaders).json(); | |
// For now, we just take the first of the lot and will improve on this in the next step | |
tweet = recentTweets.data[0]; | |
} | |
// The tweet can be empty here so we short-circuit | |
if (!tweet) { | |
// Note: you may want to disable this log message before deploying, but it's useful for development! | |
log.info('No new tweets.'); | |
// No tweet so no need to hit Mastodon, we end the script here early | |
return ''; | |
} | |
// We now have a "message" content that came straight from Twitter! | |
let message = tweet.text; | |
// The Mastodon API endpoint we need to call to post a new status update | |
let url = `https://${MASTODON_INSTANCE}/api/v1/statuses`; | |
// We configure some headers to authenticate our request and set the request type to that expected by the Mastodon API | |
let headers = { | |
'Authorization': 'Bearer '+MASTODON_ACCESS_TOKEN, | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}; | |
// This Mastodon API uses classic "HTTP form" encoding (and not e.g. JSON) so we encode our POST data in the correct format | |
let body = 'status='+encodeURIComponent(message); | |
// Toot away! | |
let mastodonResult = http.post(url, body, headers).json(); | |
// We store the id of the tweet we just posted | |
cache.set('LAST_TWEET_ID', tweet.id, 0); | |
log.info('Sent Mastodon status update: '+mastodonResult.url); | |
return mastodonResult.url; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment