Post your RSS feed to X.com (Twitter) using Slack
Many social media automation platforms now charge for automatically posting an RSS feed to X.com (Formerly Twitter). One alternative to paid social media automation platforms is to use Slack. If you are already paying for Slack Pro this comes at no additional cost. In this post we will have a look at setting up a Slack platform app to post our JDriven blog RSS feed to X.com.
Introduction
This post is not an introduction to Slack platform apps, but rather a guide for those who are considering using Slack to doing something similar. If you are new to Slack platform apps, I encourage you to review Slack’s comprehensive tutorials and documentation here
Manifest
All Slack platform apps start out with a manifest file that stipulates its requirements. This includes permissions, outgoing domains, workflows, datastore’s.
import { Manifest } from "deno-slack-sdk/mod.ts";
import { MessageDatastore } from "./datastores/messages.ts";
import { PostBlogJobWorkflow } from "./workflows/post_blog_job.ts";
export default Manifest({
name: "jdriven-blog-poster",
description: "An automatic bot that posts content to twitter",
icon: "assets/jdriven_logo.png",
// Our workflow
workflows: [PostBlogJobWorkflow],
// List of external domains our function calls
outgoingDomains: ["blog.jdriven.com", "api.twitter.com", "raw.githubusercontent.com"],
// Our datastore
datastores: [MessageDatastore],
// Permissions required
botScopes: [
"commands",
"chat:write",
"chat:write.public",
"datastore:read",
"datastore:write",
"channels:read",
"triggers:write",
"triggers:read",
],
});
Trigger
The trigger stipulates what would cause this workflow to start. In this case we would like it to be scheduled and run once an hour.
import { Trigger } from "deno-slack-sdk/types.ts";
import { TriggerTypes } from "deno-slack-api/mod.ts";
import PostBlogJobWorkflow from "../workflows/post_blog_job.ts";
const postBlogJobTrigger: Trigger<typeof PostBlogJobWorkflow.definition> =
{
type: TriggerTypes.Scheduled,
name: "Post blog job trigger",
description: "Trigger a job to check if any new posts have been made and tweet them",
workflow: `#/workflows/${PostBlogJobWorkflow.definition.callback_id}`,
inputs: {},
schedule: {
start_time: new Date(new Date().getTime() + 10000).toISOString(),
end_time: "2037-12-31T23:59:59Z",
// How often should we trigger our workflow
frequency: { type: "hourly", repeats_every: 1 },
},
};
export default postBlogJobTrigger;
Workflow
Our workflow consists of a single step, which is the PostBlogFunctionDefinition
described below.
import { DefineWorkflow } from "deno-slack-sdk/mod.ts";
import { PostBlogFunctionDefinition } from "../functions/post_blog.ts";
export const PostBlogJobWorkflow = DefineWorkflow({
callback_id: "post_blog_job",
title: "Post blog job",
description: "Post new blogs to twitter",
input_parameters: { properties: {}, required: [] },
});
// Point our workflow to our function
PostBlogJobWorkflow.addStep(PostBlogFunctionDefinition, {});
export default PostBlogJobWorkflow;
Datastore
Our datastore
is relatively simple and will include only one field, which is the id
of the posted blog.
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";
export const MessageDatastore = DefineDatastore({
name: "messages",
primary_key: "id",
attributes: {
id: {
type: Schema.types.string,
}
},
});
Utils
Let’s create a few utility functions that we can use in our Slack function.
The CONSUMER_KEY
, CONSUMER_SECRET
, ACCESS_TOKEN
, and ACCESS_SECRET
can all be retrieved from your X.com developer portal.
The CHANNEL_ID
is the id
of the channel you would like to send the message to.
import { parse } from "https://deno.land/x/xml/mod.ts"
import * as oauth from "https://raw.githubusercontent.com/snsinfu/deno-oauth-1.0a/main/mod.ts";
// Only check posts that are not older than 2 days
// Only if the post is not in the datastore will we consider it else it has already been posted
export async function isIneligibleToPost(client: any, feedItem: any) {
var twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
var publishedDate = new Date(feedItem.published);
if (publishedDate < twoDaysAgo) {
console.info("Blog is too old to consider " + feedItem.id)
return true;
}
const prePostedMessage = await client.apps.datastore.get({
datastore: "messages",
id: feedItem.id,
});
if (prePostedMessage.item.id!!) {
console.info("Blog has already been posted " + feedItem.id)
return true;
}
return false;
}
// A method to retrieve the RSS feed using a get request
export async function getFeed(client: any, env: any) {
let resultFeedXml = await fetch("https://blog.jdriven.com/atom.xml").then(response => {
if (response.ok) {
return response;
}
console.error("Failed to retrieve blog feed");
console.error(response);
postMessageToChannel(client, env, "Failed to retrieve blog feed");
return response;
}).then(response => response.text());
return parse(resultFeedXml);
}
// Method to add a message to a channel
export async function postMessageToChannel(client: any, env: any, text: string) {
await client.chat.postMessage({
channel: env["CHANNEL_ID"],
text: text
});
}
// Call the X.com API
export async function postToTwitter(env: any, tweet: string) {
let tweetBody = {
text: tweet
}
const oathClient = new oauth.OAuthClient({
consumer: {
key: env["CONSUMER_KEY"],
secret: env["CONSUMER_SECRET"]
},
signature: oauth.HMAC_SHA1,
});
const auth = oauth.toAuthHeader(oathClient.sign(
"POST",
"https://api.twitter.com/2/tweets",
{
token: {
key: env["ACCESS_TOKEN"],
secret: env["ACCESS_SECRET"]
},
body: JSON.stringify(tweetBody),
}
));
console.info("Tweeting: " + tweetBody.text);
return await fetch("https://api.twitter.com/2/tweets", {
method: "POST",
body: JSON.stringify(tweetBody),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: auth
},
});
}
Function
Our slack function is fairly simple and does the following:
-
A
GET
request is made for the RSS feedXML
. -
The list of feed blog posts is iterated through one by one.
-
If a blog post has been posted before it is ignored (This is done by querying the
datastore
). -
If the blog post hasn’t been posted before, post it to X.com and add it to the
datastore
of posted blogs.
import { DefineFunction, SlackFunction } from "deno-slack-sdk/mod.ts";
import { getFeed, postToTwitter, isIneligibleToPost, postMessageToChannel } from "./utils.ts";
export const PostBlogFunctionDefinition = DefineFunction({
callback_id: "post_blog",
title: "Post new blog post to Twitter",
source_file: "functions/post_blog.ts",
input_parameters: { properties: {}, required: [] },
output_parameters: { properties: {}, required: [] },
});
interface Blog {
feed: Feed;
}
interface Feed {
entry: any[]
}
export default SlackFunction(
PostBlogFunctionDefinition,
async ({ client, env }) => {
let resultFeedJson: Blog = await getFeed(client, env) as unknown as Blog;
let feed: Feed = resultFeedJson.feed;
// Loop over all blog posts in RSS Feed
for (const feedItem of feed.entry) {
// If post is ineligible skip over it
if (await isIneligibleToPost(client, feedItem)) {
continue;
}
// Post is eligible so let's post it
let twitterResponse = await postToTwitter(env, feedItem.title + " " + feedItem.id);
if (twitterResponse.ok) {
// If successfully posted add to datastore of posted blogs
await client.apps.datastore.put({
datastore: "messages",
item: {
id: feedItem.id
},
});
let twitterBody = await twitterResponse.json()
console.info("Tweet posted for blog post: https://twitter.com/jdriven_nl/status/" + twitterBody.data.id)
await postMessageToChannel(client, env,
"Tweet posted for blog post: https://twitter.com/jdriven_nl/status/" + twitterBody.data.id);
} else {
console.error("Failed to Tweet blog post: " + feedItem.id + " Response: " + twitterResponse.body)
console.error(twitterResponse)
await postMessageToChannel(client, env, "Failed to Tweet blog post: " + feedItem.id);
}
}
return { outputs: {} };
},
);
Conclusion
After deploying our app using the Slack cli
, if a new blog post appears in the RSS feed and the trigger runs, it will automatically be posted to X.com.
A message will also be sent on Slack.