Yes, I built an application - Twidderly 🐥 (couldn't find something good) that will post Tweets on my behalf.
And the best part is that it doesn't just post random stuff.
💡 It converts my LinkedIn posts to tweets.
Let's see how I built this. But before that one thing : Do not misuse this build
. I will be posting a guide to add one for your self.
A Sample before the actual process
What you will need?
Before actually getting into the code, I want to share what you will need to build the application.
npm
installed on your system : I have used NextJS with Typescript to build Twidderly.Appwrite
Web App configured : I am using Appwrite Authentication and Database to store Tweets to be posted.Gemini API Key
: Gemini converts the LinkedIn post to Tweets and also generates 2 other tweets for each day.Twitter (or X) API
tokens and secrets : The most important part of this Application is to Tweet, using the API
The Setup Part
For the setup part, I will let you go through guides that helped me setup the project and the requirements easily.
⚙️ Setup
NodeJS
andnpm
on Windows and Mac⚙️ Setup
Typescript
withnpm
andVS Code
⚙️ Setup Twitter API with NodeJS(works with NextJS API)
⚙️ Appwrite Setup (We only Need Authentication and Databases)
The Fun Part
Let's go through the fun part of the Application - Developing the Important Part of the Application
The Front End is built to do only 3 things :
Authenticate the User :
For this I used Appwrite Client to register and authenticate users before they can access the Post scheduling endpoint.
The Appwrite Client is exposed from the utilities/appwrite/index.ts
file :
const getAppwriteClient = () => {
const appwriteEndpoint : string = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
const appwriteProjectID : string = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!;
// console.log(appwriteEndpoint,appwriteProjectID);
const client = new Client();
client
.setEndpoint(appwriteEndpoint)
.setProject(appwriteProjectID);
return client;
}
export {getAppwriteClient};
This exposed client is used in the auth/login/page.tsx
and auth/signup/page.tsx
files for Authentication :
const handleLogin:FormEventHandler = async (e : FormEvent) => {
e.preventDefault();
const formData = new FormData(formRef.current!);
const email : string = formData.get("email")?.toString() || "";
const password : string = formData.get("password")?.toString() || "";
// if(email!=="thevinitgupta@gmail.com"){
// return alert("You are not Authorized!")
// }
// console.log(email,password)
setLoading(true);
try {
const client = getAppwriteClient();
const account = new Account(client);
const session = await account.createEmailSession(email, password);
window.location.assign("/posts/create")
} catch (error) {
alert(error);
}
setLoading(false);
}
Creating Posts
Once the user is Authenticated, the next part is the ability to create and schedule posts.
For scheduling, I am using Github Actions
which I will explain in the last section.
For creating posts, I am using Appwrite and Gemini. Let's see how.
For the first step, I get the LinkedIn post from the posts/create/page.tsx
file and then:
async function handlePost(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
const formData = new FormData(formRef.current!);
const post: string = formData.get("post")?.toString() || "";
if (post.length === 0) {
alert("Cannot Add Empty!");
return;
}
. . .
}
Once I have the post, I need to first convert it to Tweets using the Gemini API route I create(we'll see it after this) and then added the different tweets to Appwrite Database storage :
async function handlePost(event: FormEvent<HTMLFormElement>): Promise<void> {
. . .
setLoading(true);
try {
const { data } = await axios.post("/api/gemini", {
linkedinPost: post
});
const posts : Array<string> = data.split("|");
// console.log(posts, typeof posts);
let docIds : Array<string> = [];
posts.forEach(async (tweet) => {
const docId = await createDocument(tweet);
if (docId === "failed") {
throw new Error("Not Created!")
}
docIds.push(docId);
});
alert("Posts create : "+docIds.length)
} catch (error) {
// console.log(error)
}
setLoading(false)
}
Viewing the Currently scheduled Posts
Once your posts are scheduled, you can see the list of posts scheduled by you. You can also delete any post from the list and it is removed from the Database.
const getCurrentUser = async () => {
setAuthLoading(true);
try {
const client = getAppwriteClient();
const account = new Account(client);
const session = await account.get();
setUsername(session.name);
setAuthLoading(false);
getPosts(session.$id);
} catch (error) {
// console.log("error getting user")
setAuthLoading(false);
window.location.assign("/auth/login")
}
}
useEffect(()=> {
getCurrentUser();
},[username])
I have also added the deletion function with each function that allows you to remove posts that you don't want to schedule :
const handleDelete = async (postId : string) => {
const deletionResult = await deletePost(postId);
if(deletionResult==="success"){
const newPosts = posts.filter((post) => post.$id!==postId);
setCurrentPosts(newPosts);
}
else {
alert("Deletion failed!")
}
}
Now that the frontend is done, it's time to explore the code that is powering all of this.
The Process Flow
Before getting into the code, let's first understand the code flow :
- User puts the LinkedIn post, which is sent to the
api/gemini
API route. - At this point, the post is sent to Gemini with the following prompt :
Analyze the provided text and generate 3 concise and engaging Twitter posts that capture the key points and essence of the content, suitable for Twitter's character limit (280 characters) and audience. Each tweet should be a complete thought and avoid cliffhangers. Maintain a consistent non-formal voice and tone throughout the tweets. Do not change the tone of the Post. DONT ADD ANY EXTRA TEXT LIKE TWEET 1, TWEET 2, etc. Just give 3 tweets, each separated by 2 newlines
- The Response returned by Gemini is divided into 3 parts and sent to the
createDocument()
function that adds these to Appwrite Database
Code for the above steps
The api/gemini
route uses the function exposed by utilities/gemini/index.ts
file that processes the posts :
async function convertToTweet(linkedInPost : string, geminiApiKey : string) {
// console.log(geminiApiKey);
const model = getGeminiModel(geminiApiKey);
const prompt = "Analyze the provided text and generate 3 concise ... Just give 3 tweets, each separated by 2 newlines"
const result = await model.generateContent([prompt,linkedInPost]);
const response = await result.response;
const tweet = response.text();
const tweetSeparator = /\n\n/; // Two newlines for separation
const tweets = tweet.split(tweetSeparator);
// console.log(tweets);
return tweets.join("|");
}
The posts are added to the Appwrite Database using a for-loop
which calls the below function for each post from the posts/schedule/page.tsx
front-end file :
const createDocument = async (tweet : string) : Promise<string> => {
const dbId : string = process.env.NEXT_PUBLIC_APPWRITE_DATABASE!;
const collectionId : string = process.env.NEXT_PUBLIC_APPWRITE_COLLECTION!;
const client = getAppwriteClient();
try {
const account = new Account(client);
const user = await account.get();
const db = await new Databases(client);
const docRef = await db.createDocument(dbId,collectionId, ID.unique(), {
scheduledOn : new Date(),
post: tweet,
owner : user.$id
}, [
Permission.write(Role.users()), // only users can write to documents
Permission.read(Role.any()), // Anyone can view this document
Permission.update(Role.user(user.$id)), // Current user can only Update the Document
Permission.delete(Role.user(user.$id)), // Current user can only Delete the Document
]);
return docRef.$id;
} catch (error) {
// console.log(error);
return "failed";
}
}
The create document takes in 5 parameters :
- DatabaseID
- CollectionID
- A Unique ID for the new Document
- The actual Document, where I have added the Owner of the Document
- Permissions array, without with the Document is not added.
🚨 You can understand more about Document Permissions in Appwrite Here 🔗
Once these posts are added, you can relax.
Because the Posting is done using Github Scheduled Actions as we see below
The Tweeting API
Until now we saw how to setup the whole App to be able to create Posts for a User. Now for the main part : The Tweeting of Tweets on Twitter
or The X-ing of X-es on X
.
Once you have setup the X-Developer Options and installed the following npm
library :
npm install twitter-api-v2
We are ready.
Posting to Twitter is as simple as this :
- Setup the Twitter Client :
const getTwitterClient = () => {
// console.log("Hello")
const apiKey : string = process.env.TWITTER_API_KEY || "";
const consumer_secret : string = process.env.TWITTER_API_SECRET || "";
const access_token : string = process.env.TWITTER_ACCESS_TOKEN || "";
const access_token_secret : string = process.env.TWITTER_TOKEN_SECRET || "";
const bearerToken : string = process.env.TWITTER_BEARER_TOKEN || "";
const client = new TwitterApi({
appKey : apiKey,
appSecret : consumer_secret,
accessToken : access_token,
accessSecret : access_token_secret
});
const bearer = new TwitterApi(bearerToken);
const twitterClient = client.readWrite;
const twitterBearer = bearer.readOnly;
return {
readWrite : twitterClient,
readOnly : twitterBearer
}
}
- Send the Tweet
const postTweet = async ({post} : {post : string}) => {
const {readWrite} = getTwitterClient();
try {
const response = await readWrite.v2.tweet(post);
return response.data;
} catch (error) {
// console.log(error)
return error;
}
}
That's it for the actual tweeting.
But for scheduling the Posts, I need to expose an API that I will call using Github Actions at certain times of the day.
The API does 3 things :
- Check if the
Request is Valid
. - Checking the Time of Day
- Posting a Tweet based on the Time : LinkedIn Tweet, Programming Joke or Tech News.
The Code is pretty self explanatory as you can see below :
export async function POST(req : NextRequest){
const request = await req.json();
const {encryptedData} = request as TweetBody;
if(!encryptedData) {
return new Response("Invalid Request", {
status : 403,
});
}
const {userId} = decryptData(encryptedData);
if(!checkIfUserAllowed(userId)) throw new Error("Invalid User")
const currentHour: number = new Date().getHours();
try {
// Switch based on time
if(currentHour>=8 && currentHour<9) {
await createFromTechJoke()
}
else if(currentHour>=14 && currentHour<15){
await createFromTechNews();
}
else {
await createFromUserPosts(userId);
}
return new Response("Done!!", {
status : 200,
});
} catch (error) {
return new Response("Not Tweeted!!", {
status : 500,
});
}
}
Scheduling using Github Actions
One of the best features of GitHub as a service are the Actions, which we can use to schedule tasks.
We can use cURL
to call the API along with a Secret used to identify if it's a valid Request.
The action is Scheduled to run everyday 5 times which is defined as :
on:
schedule:
- cron: '0 4,8,11,14,16 * * *' # Scheduled at 10am, 2pm, 5pm, 8pm, and 10pm IST
The complete tweet.yml
file is as below :
name: Daily Tweet Scheduler
on:
schedule:
- cron: '0 4,8,11,14,16 * * *' # Scheduled at 10am, 2pm, 5pm, 8pm, and 10pm IST
jobs:
post-tweet:
runs-on: ubuntu-latest
steps:
- name: Post Tweet
run: |
curl -X POST https://twidder.vercel.app/api/twitter \
-H "Content-Type: application/json" \
-d '{"encryptedData": "${{ secrets.TWITTER_POSTER_KEY }}"}'
📹 You can check this video out for the Complete Explanation on Setting up actions for your Repositories : GitHub Actions
I have not updated the README.md
file yet, where I will be adding the setup instructions after this to configure Twidderly for your own posts.
Do star the Twidderly ⭐️ to get the Update. Also, I am open to any contributions to make the Application Better.
Top comments (2)
Nice job! Thanks for sharing!!!
Thanks Leandro!!