Skip to content

Instantly share code, notes, and snippets.

@flaki
Last active June 20, 2022 05:58
Show Gist options
  • Save flaki/d53fb0e83ebbf3ca86c8fe4bb52ad0fa to your computer and use it in GitHub Desktop.
Save flaki/d53fb0e83ebbf3ca86c8fe4bb52ad0fa to your computer and use it in GitHub Desktop.
Creating a Twitter-Mastodon crossposter in WebAssembly with Atmo
// 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';
};
// 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';
};
// 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