Tweets Scheduler Built using NextJS, Gemini API, Appwrite and Twitter API
Tags: productivity, api, nextjs, appwritehack
Published: 2024-03-25 14:35:33

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
<video src="https://github.com/thevinitgupta/100-Days-of-Learning/assets/65801700/8559bf35-515e-4f47-a8a9-0033c109bb93" autoplay=true alt="Twidderly Sample Video" controls=true />

## 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 ](https://radixweb.com/blog/installing-npm-and-nodejs-on-windows-and-mac)

> ⚙️  [Setup `Typescript` with `npm` and `VS Code`](https://www.positronx.io/setup-typescript-using-npm-and-visual-studio-code/)

> ⚙️ [Setup Twitter API with NodeJS(works with NextJS API)](https://youtu.be/LCydSB7JcHw?si=QrW57PhL8vPyFhT8)

> ⚙️ [Gemini API Setup](https://www.youtube.com/watch?v=VakUoX_dZV0&t=207s)

> ⚙️ [Appwrite Setup (We only Need Authentication and Databases)](https://medium.com/@dprincecoder/how-to-use-appwrite-as-a-backend-service-buiding-a-chat-app-with-appwrite-fed6c4fce6ee)


## 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 : 
```ts
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 : 

```tsx
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: 
```tsx
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 : 
```tsx
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.

```tsx
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 : 
```tsx
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 : 
```js
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 : 

```ts
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 : 

```ts
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`.
<img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHdncHFkb3RncnpjNXNqeGM3d3BtbG5qOXk2dmNqZGx5NTl1cm1vbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/vr79YbbbsbEwHZ4Dhi/giphy.gif" alt="elon on Twitter"/>

Once you have setup the X-Developer Options and installed the following `npm` library : 
```js
npm install twitter-api-v2
```

We are ready. 

Posting to Twitter is as simple as this : 
- Setup the Twitter Client :
```ts
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
```ts
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 : 
```ts
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,
        });
      }
}
```
<img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExaGVqcnJ0MHdmYnkyZ244MTdieGJibHZyamNsa3NnaWw0OWdlYjgwdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/DiE5Buu4rXpKPHS6kV/giphy.gif" alt="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 : 
```ts
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 : 
```ts
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](https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://www.youtube.com/watch%3Fv%3DylEy4eLdhFs&ved=2ahUKEwjByt_szI-FAxWnRmcHHXNsD-EQwqsBegQIExAG&usg=AOvVaw2KUR2XDZXlILCdGOkq4c_W)


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 ⭐️](https://github.com/thevinitgupta/Twidder) to get the Update. Also, I am open to any contributions to make the Application Better.