DEV Community

Cover image for Tweets Scheduler Built using NextJS, Gemini API, Appwrite and Twitter API
Vinit Gupta
Vinit Gupta

Posted on

Tweets Scheduler Built using NextJS, Gemini API, Appwrite and Twitter API

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 and npm on Windows and Mac

⚙️ Setup Typescript with npm and VS Code

⚙️ Setup Twitter API with NodeJS(works with NextJS API)

⚙️ Gemini API Setup

⚙️ 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};
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }

  . . .
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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!")
        }

    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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("|");
}
Enter fullscreen mode Exit fullscreen mode

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";
    }

}
Enter fullscreen mode Exit fullscreen mode

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.
elon on Twitter

Once you have setup the X-Developer Options and installed the following npm library :

npm install twitter-api-v2
Enter fullscreen mode Exit fullscreen mode

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
      }

}
Enter fullscreen mode Exit fullscreen mode
  • 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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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,
        });
      }
}
Enter fullscreen mode Exit fullscreen mode

almost there

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
Enter fullscreen mode Exit fullscreen mode

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 }}"}'

Enter fullscreen mode Exit fullscreen mode

📹 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)

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Nice job! Thanks for sharing!!!

Collapse
 
thevinitgupta profile image
Vinit Gupta

Thanks Leandro!!