DEV Community

Cover image for The project I built with the Twitter API, GPT4 & CopilotKit…🚀🧑‍💻
Nevo David Subscriber for Gitroom

Posted on • Updated on

The project I built with the Twitter API, GPT4 & CopilotKit…🚀🧑‍💻

TL;DR

Last week I built an application that automates my social media posting using AI. I will show you how I did it.

We will cover how to:

  • add Twitter authentication to a Next.js application,
  • create a calendar-like interface from scratch,
  • integrate AI assistants into software applications with CopilotKit,
  • create action-specific AI copilots to handle various tasks within the application, and
  • build a post generator and scheduling application.

This project is a great way to learn how to build AI-powered apps and master social media APIs, but don't use it to be mean 😈

It's a me, Elon


CopilotKit: The framework for building in-app AI copilots

CopilotKit is an open-source AI copilot platform. We make it easy to integrate powerful AI into your React apps.

Build:

  • ChatBot: Context-aware in-app chatbots that can take actions in-app 💬
  • CopilotTextArea: AI-powered textFields with context-aware autocomplete & insertions 📝
  • Co-Agents: In-app AI agents that can interact with your app & users 🤖

Star CopilotKit

Star CopilotKit ⭐️


Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

We'll also make use of the following:

  • CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.
  • Redis - an in-memory database for storing the post schedule.
  • BullMQ - a Node.js library that manages and processes jobs in a queue.
  • Node Cron - a Node.js library that schedule and runs tasks (jobs) at specific intervals.
  • Headless UI - for creating accessible UI components for the application.
  • X Client ID and Secret - for authenticating users and creating posts on their behalf.
  • OpenAI API Key - to enable us to perform various tasks using the GPT models.

Project Set up and Package Installation

First, create a Next.js application by running the code snippet below in your terminal:

npx create-next-app social-media-scheduler
Enter fullscreen mode Exit fullscreen mode

Select your preferred configuration settings. For this tutorial, we'll be using TypeScript and Next.js App Router.

Creating the Next.js application

Next, install the project dependencies:

npm install @headlessui/react lodash bullmq ioredis node-cron
Enter fullscreen mode Exit fullscreen mode

Finally, install the required CopilotKit packages. These packages enable us to use AI auto-completion within the application, allow the AI copilot to retrieve data from the React state, and make decisions within the application.

npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
Enter fullscreen mode Exit fullscreen mode

Congratulations! You're now ready to build the application.


Building the Posts Scheduler App with Next.js

In this section, you'll learn how to create the user interface for the scheduling application. The application is divided into two pages: the Login page and the Dashboard page, where users can create and schedule posts.

The Login page authenticates users using their X (Twitter) profile, while the Dashboard page allows users to create, delete, and schedule posts.

The Login Page

The Login page represents the application's home page. Users need to sign in with their Twitter account to access the dashboard.

To implement this, update the page.tsx file to display a sign-in button as shown below:

import Link from "next/link";
import { getTwitterOauthUrl } from "@/app/util";

export default function Home() {
    return (
        <main className='w-full min-h-screen flex flex-col items-center justify-center p-8'>
            <h2 className='font-semibold text-2xl mb-4'>Your AI Post Scheduler</h2>
            <Link
                href={getTwitterOauthUrl()}
                className='bg-black py-3 px-6 hover:bg-gray-700 text-gray-50 rounded-lg'
            >
                Sign in with Twitter
            </Link>
        </main>
    );
}
Enter fullscreen mode Exit fullscreen mode

The code snippet above displays a Sign in with Twitter button that redirects users to Twitter Oauth2 page. You'll learn how to setup the Twitter authentication shortly.

Login Button

The Dashboard Page

Before we proceed, create a types.d.ts file at the root of the Next.js project. This file will contain the type declarations for the variables within the application.

interface DelSelectedCell {
    content?: string;
    day_id?: number;
    day?: string;
    time_id?: number;
    time?: string;
    published?: boolean;
    minutes?: number;
}
interface SelectedCell {
    day_id?: number;
    day?: string;
    time_id?: number;
    time?: string;
    minutes?: number;
}

interface Content {
    minutes?: number;
    content?: string;
    published?: boolean;
    day?: number;
}

interface AvailableScheduleItem {
    time: number;
    schedule: Content[][];
}
Enter fullscreen mode Exit fullscreen mode

Create a utils file within the Next.js app folder and copy this code snippet from the GitHub repository into it. It contains the necessary functions for performing various data manipulations within the application.

Next, create a dashboard folder containing a page.tsx file within the Next.js app directory.

cd app
mkdir dashboard && cd dashboard
touch page.tsx
Enter fullscreen mode Exit fullscreen mode

Copy the code snippet below into the dashboard/page.tsx file. It renders an App component that accepts the application's schedule as props and displays them in a table:

"use client";
import _ from "lodash";
import { useState } from "react";
import App from "@/app/components/App";
import { availableSchedule } from "../util";

export default function Dashboard() {
    //👇🏻 saves a deep copy of the availableSchedule array into the React state
    const [yourSchedule, updateYourSchedule] = useState<AvailableScheduleItem[]>(
        _.cloneDeep(availableSchedule)
    );

    return (
        <App yourSchedule={yourSchedule} updateYourSchedule={updateYourSchedule} />
    );
}
Enter fullscreen mode Exit fullscreen mode

Post Schedule

Here is the data structure for the table above:

export const tableHeadings: string[] = [
    "Time",
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
];

export const availableSchedule: AvailableScheduleItem[] = [
    {
        time: 0,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 1,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 2,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 3,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 4,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 5,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 6,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 7,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 8,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 9,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 10,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 11,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 12,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 13,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 14,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 15,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 16,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 17,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 18,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 19,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 20,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 21,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 22,
        schedule: [[], [], [], [], [], [], []],
    },
    {
        time: 23,
        schedule: [[], [], [], [], [], [], []],
    },
];
Enter fullscreen mode Exit fullscreen mode

The tableHeadings array contains the headings for the table columns, while the availableSchedule array holds a group of objects. Each object has a time property representing each hour of the day and a schedule property containing a nested array, with each element representing a day of the week.

For example, when a user sets a schedule for Wednesday at 8 AM, the application searches for the object with a time property of 8 and updates its schedule property by inserting the schedule into the nested array at the fourth index.

You can copy the remaining UI elements for the Dashboard page from its GitHub repository.

In the upcoming sections, you'll learn how to add Twitter OAuth and CopilotKit to the application.


How to add X Authentication to your Next.js application

In this section, you’ll learn how to create a X Developer project and add X authentication your Next.js applications.

Ensure you have an X account and visit the X Developers' Portal to create a new project.

Create X Developer Project

Enter the project name and provide answers to the required questions to create a new project and an app.

X Developer Project

Set up the user authentication settings to allow you read and write posts on behalf of the users.

X Authentication Settings

Finally, fill the App info section accordingly.

X App Info section

After setting up the authentication process, save the OAuth 2.0 Client ID and secret into a .env.local file.

TWITTER_CLIENT_ID=<your_client_ID>
NEXT_PUBLIC_TWITTER_CLIENT_ID=<your_client_ID>
TWITTER_CLIENT_SECRET=<your_client_Secret>
Enter fullscreen mode Exit fullscreen mode

Authenticating users via X

Create an api folder within the Next.js app folder. Inside the api folder, create a twitter directory containing a route.ts file. This will create an API endpoint (/api/twitter) that enables us to authenticate users.

cd app
mkdir api && cd api
mkdir twitter && cd twitter
touch route.ts
Enter fullscreen mode Exit fullscreen mode

Copy the code snippet below into the route.ts file:

import { NextRequest, NextResponse } from "next/server";

const BasicAuthToken = Buffer.from(
    `${process.env.TWITTER_CLIENT_ID!}:${process.env.TWITTER_CLIENT_SECRET!}`,
    "utf8"
).toString("base64");

const twitterOauthTokenParams = {
    client_id: process.env.TWITTER_CLIENT_ID!,
    code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
    redirect_uri: `http://www.localhost:3000/dashboard`,
    grant_type: "authorization_code",
};

//👇🏻 gets user access token
export const fetchUserToken = async (code: string) => {
    try {
        const formatData = new URLSearchParams({
            ...twitterOauthTokenParams,
            code,
        });
        const getTokenRequest = await fetch(
            "https://api.twitter.com/2/oauth2/token",
            {
                method: "POST",
                body: formatData.toString(),
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    Authorization: `Basic ${BasicAuthToken}`,
                },
            }
        );
        const getTokenResponse = await getTokenRequest.json();
        return getTokenResponse;
    } catch (err) {
        return null;
    }
};

//👇🏻gets user's data from the access token
export const fetchUserData = async (accessToken: string) => {
    try {
        const getUserRequest = await fetch("https://api.twitter.com/2/users/me", {
            headers: {
                "Content-type": "application/json",
                Authorization: `Bearer ${accessToken}`,
            },
        });
        const getUserProfile = await getUserRequest.json();
        return getUserProfile;
    } catch (err) {
        return null;
    }
};

//👉🏻 API endpoint utilizing the functions above
Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above,
    • The BasicAuthToken variable contains the encoded version of your tokens.
    • The twitterOauthTokenParams contains the parameters required for getting the users' access token.
    • The fetchUserToken function sends a request containing a code to Twitter's endpoint and returns the user's access token.
    • The fetchUserData function uses the token to retrieve the user's X profile.

Add this endpoint below the functions. It accepts a code from the frontend when a user signs in and stores the user ID, username, and access token in a file that can be accessed when executing jobs on the server.

import { writeFile } from "fs";

export async function POST(req: NextRequest) {
    const { code } = await req.json();
    try {
        //👇🏻 get access token and the entire response
        const tokenResponse = await fetchUserToken(code);
        const accessToken = await tokenResponse.access_token;
        //👇🏻 get user data
        const userDataResponse = await fetchUserData(accessToken);
        const userCredentials = { ...tokenResponse, ...userDataResponse };

        //👇🏻  merge the user's access token, id, and username into an object
        const userData = {
            accessToken: userCredentials.access_token,
            _id: userCredentials.data.id,
            username: userCredentials.data.username,
        };
        //👇🏻 store them in a JSON file (for server-use)
        writeFile("./src/user.json", JSON.stringify(userData, null, 2), (error) => {
            if (error) {
                console.log("An error has occurred ", error);
                throw error;
            }
            console.log("Data written successfully to disk");
        });
        //👇🏻 returns a successful response
        return NextResponse.json(
            {
                data: "User data stored successfully",
            },
            { status: 200 }
        );
    } catch (err) {
        return NextResponse.json({ error: err }, { status: 500 });
    }
}
Enter fullscreen mode Exit fullscreen mode

Update the dashboard/page.tsx to send the code to the API endpoint after authenticating a user.

import { useSearchParams } from 'next/navigation'
const searchParams = useSearchParams()
    const code = searchParams.get('code')

    const fetchToken = useCallback(async () => { 
            const res = await fetch("/api/twitter", {
                method: "POST",
                body: JSON.stringify({ code }),
                headers: {
                    "Content-Type": "application/json",
                },
            });
            if (res.ok) {
                const data = await res.json();
                console.log(data);
            }
    }, [code]);

    useEffect(() => {
        fetchToken();
    }, [fetchToken]);
Enter fullscreen mode Exit fullscreen mode

Congratulations! When users click the Sign in with Twitter button, it redirects them to the Twitter authorisation page to enable them access the application.

X Authentication Preview


How to add CopilotKit to a Next.js application

In this section, you'll learn how to add CopilotKit to the application to enable users to schedule posts automatically using AI copilots and also add auto-completion when creating post contents.

Before we proceed, visit the OpenAI Developers' Platform and create a new secret key.

OpenAI API

Create a .env.local file and copy the your newly created secret key into the file.

OPENAI_API_KEY=<YOUR_OPENAI_SECRET_KEY>
OPENAI_MODEL=gpt-4-1106-preview
Enter fullscreen mode Exit fullscreen mode

Next, you need to create an API endpoint for CopilotKit. Within the Next.js app folder, create an api/copilotkit folder containing a route.ts file.

cd app
mkdir api && cd api
mkdir copilotkit && cd copilotkit
touch route.ts
Enter fullscreen mode Exit fullscreen mode

Copy the code snippet below into the route.ts file. The CopilotKit backend accept users’ requests and make decisions using the OpenAI model.

import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/backend";

export const runtime = "edge";

export async function POST(req: Request): Promise<Response> {
    const copilotKit = new CopilotRuntime({});
    const openaiModel = process.env["OPENAI_MODEL"];

    return copilotKit.response(req, new OpenAIAdapter({model: openaiModel}));
}
Enter fullscreen mode Exit fullscreen mode

To connect the application to the backend API route, copy the code snippet below into the dashboard/page.tsx file.

"use client";
import App from "@/app/components/App";
import _ from "lodash";
import {  useState } from "react";
import { availableSchedule } from "../util";

//👇🏻 CopilotKit components
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";

//👇🏻 CSS styles for CopilotKit components
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";

export default function Dashboard() {
    const [yourSchedule, updateYourSchedule] = useState<AvailableScheduleItem[]>(
        _.cloneDeep(availableSchedule)
    );

    //👉🏻 other UI states and functions

    return (
        <CopilotKit runtimeUrl='/api/copilotkit/'>
            <App
                yourSchedule={yourSchedule}
                updateYourSchedule={updateYourSchedule}
            />
            <CopilotPopup
                instructions='Help the user create and manage ad campaigns.'
                defaultOpen={true}
                labels={{
                    title: "Posts Scheduler Copilot",
                    initial:
                        "Hello there! I can help you manage your schedule. What do you want to do? You can generate posts, add, and delete scheduled posts.",
                }}
                clickOutsideToClose={false}
            ></CopilotPopup>
         </CopilotKit>
    );
}
Enter fullscreen mode Exit fullscreen mode

The CopilotKit component wraps the entire application and accepts a runtimeUrl prop that contains a link to the API endpoint. The CopilotKitPopup component adds a chatbot sidebar panel to the application, enabling us to provide various instructions to CopilotKit.

App Overview


How to schedule posts with CopilotKit

CopilotKit provides two hooks that enable us to handle user's request and plug into the application state: useCopilotAction and useCopilotReadable.

The useCopilotAction hook allows you to define actions to be carried out by CopilotKit. It accepts an object containing the following parameters:

  • name - the action's name.
  • description - the action's description.
  • parameters - an array containing the list of the required parameters.
  • render - the default custom function or string.
  • handler - the executable function that is triggered by the action.
useCopilotAction({
    name: "sayHello",
    description: "Say hello to someone.",
    parameters: [
        {
            name: "name",
            type: "string",
            description: "name of the person to say greet",
        },
    ],
    render: "Process greeting message...",
    handler: async ({ name }) => {
        alert(`Hello, ${name}!`);
    },
});
Enter fullscreen mode Exit fullscreen mode

The useCopilotReadable hook provides the application state to CopilotKit.

import { useCopilotReadable } from "@copilotkit/react-core";

const myAppState = "...";
useCopilotReadable({
  description: "The current state of the app",
  value: myAppState
});
Enter fullscreen mode Exit fullscreen mode

Now, let’s plug the application state into CopilotKit and create an action that helps us to schedule posts.

Within the App component, pass the schedule state into CopilotKit. You can also provide additional information (context) to enable CopilotKit make adequate and precise decisions.

//👇🏻 Application state
useCopilotReadable({
    description: "The user's Twitter post schedule",
    value: yourSchedule,
});

//👇🏻 Application context
useCopilotReadable({
    description: "Guidelines for the user's Twitter post schedule",
    value:
            "Your schedule is displayed in a table format. Each row represents an hour of the day, and each column represents a day of the week. You can add a post by clicking on an empty cell, and delete a post by clicking on a filled cell. Sunday is the first day of the week and has a day_id of 0.",
    });
Enter fullscreen mode Exit fullscreen mode

Create a CopilotKit action that schedule posts based on the user’s prompts:

    useCopilotAction({
        name: "updatePostSchedule",
        description: "Update the user's Twitter post schedule",
        parameters: [
            {
                name: "update_schedule",
                type: "object",
                description: "The user's updated post schedule",
                attributes: [
                    {
                        name: "time",
                        type: "number",
                        description: "The time of the post",
                    },
                    {
                        name: "schedule",
                        type: "object[]",
                        description: "The schedule for the time",
                        attributes: [
                            {
                                name: "content",
                                type: "string",
                                description: "The content of the post",
                            },
                            {
                                name: "minutes",
                                type: "number",
                                description: "The minutes past the hour",
                            },
                            {
                                name: "published",
                                type: "boolean",
                                description: "Whether the post is published",
                            },
                            {
                                name: "day",
                                type: "number",
                                description: "The day of the week",
                            },
                        ],
                    },
                ],
            },
        ],
        handler: ({ update_schedule }) => {
            setAddEventModal(true);
            setSelectedCell({
                day_id: update_schedule.schedule[0].day + 1,
                day: tableHeadings[update_schedule.schedule[0].day + 1],
                time_id: update_schedule.time,
                time: formatTime(update_schedule.time),
            });
            setContent(update_schedule.schedule[0].content);
            setMinute(update_schedule.schedule[0].minutes);
        },
        render: "Updating schedule...",
    });
Enter fullscreen mode Exit fullscreen mode

The code snippet above shows the useCopilotAction hook in action. It accepts an object containing name, description, parameters, handler, and render properties.

  • The name property represents the name of the action.
  • The description property provides a brief overview of what the function does.
  • The parameters array contains an update_schedule object with a time and schedule property. The schedule object includes content, minutes, published, and day attributes.
  • The handler function describes the action to be carried out when triggered. In the example above, the handler function opens the AddPost modal, updates its value with AI-generated inputs, and allows the user adjust the schedule accordingly.

Managing and Scheduling Posts using Redis and BullMQ

In this section, you’ll learn how to store the post schedule in a Redis database, and create a job that checks the schedule at intervals to post the content on X (Twitter).

First, you need to install Redis on your computer. If you are using MacOS and have Homebrew installed, run the code snippet in your terminal to install Redis:

brew --version
brew install redis
Enter fullscreen mode Exit fullscreen mode

Once the installation process is complete, you can test your Redis server by running the following code snippet in your terminal:

redis-server
Enter fullscreen mode Exit fullscreen mode

Redis Server

Now, you can use the Node.js Redis client within the application.

Create an /api/schedule API route on the server that accepts the entire schedule table when a user adds or deletes a scheduled post.

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) { 
    const { schedule } = await req.json();
    try {
         console.log({schedule})

        return NextResponse.json(
            { message: "Schedule updated!", schedule },
            { status: 200 }
        );

    } catch (error) { 
        return NextResponse.json(
            { message: "Error updating schedule", error },
            { status: 500 }
        );

    }   
}
Enter fullscreen mode Exit fullscreen mode

Update the API endpoint to store the entire schedule in the Redis database. Redis stores data in key/value pairs, making it super-fast for storing and retrieving data.

import { NextRequest, NextResponse } from "next/server";
import Redis from "ioredis";
const redis = new Redis();

export async function POST(req: NextRequest) { 
    const { schedule } = await req.json();
    try {
        //👇🏻 saves the schedule
        await redis.set("schedule", JSON.stringify(schedule));

        return NextResponse.json(
            { message: "Schedule updated!", schedule },
            { status: 200 }
        );
    } catch (error) { 
        return NextResponse.json(
            { message: "Error updating schedule", error },
            { status: 500 }
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

You can also add a GET request handler within the api/schedule/route.ts file to fetch existing scheduled posts from the Redis database and display them when a user logs into the application.

export async function GET() {
    try {
        const schedule = await redis.get("schedule");
        if (schedule) {
            return NextResponse.json(
                { message: "Schedule found", schedule: JSON.parse(schedule) },
                { status: 200 }
            );
        }
    } catch (error) {
        return NextResponse.json(
        { message: "Schedule not found" },
        { status: 500 }
    );
}
}
Enter fullscreen mode Exit fullscreen mode

Finally, you need to set up a job queue that runs every minute to check for posts scheduled for the current day and post them at the appropriate time.

Create a worker.ts file within the Next.js src folder and copy the following code into the file:

import data from "./user.json";
import { Worker, Queue } from 'bullmq';
import Redis from "ioredis";

//👇🏻 initializes a job queue connected to the Redis database
const redis = new Redis({maxRetriesPerRequest: null});
const scheduleQueue = new Queue('schedule-queue', { connection: redis });
Enter fullscreen mode Exit fullscreen mode

The code snippet above creates a job queue that is connected to the Redis database.

Implement a scheduleJobs function within the worker.ts file that gets the posts scheduled for the current time and adds them to the job queue.

//👇🏻 add jobs to the queue
export const scheduleJobs = async (schedule: AvailableScheduleItem[]) => {
    //👇🏻 gets current time and day
    const now = new Date();
    const currentHour = now.getHours();
    const currentMinute = now.getMinutes();
    const currentDay = now.getDay();

    //👇🏻 gets posts for the current hour
    const currentSchedule = schedule.find((item) => item.time === currentHour);
    const schedulesForTheHour = currentSchedule?.schedule[currentDay];

    //👇🏻 gets scheduled posts for the current time
    if (schedulesForTheHour && schedulesForTheHour?.length > 0) {
        const awaitingJobs = schedulesForTheHour.filter(
            (scheduleItem) =>
                scheduleItem.minutes && scheduleItem.minutes <= currentMinute
        );

        //👇🏻 add jobs to queue
        return awaitingJobs.map(async (scheduleItem) => {
            const job = await scheduleQueue.add("jobs", {
                message: scheduleItem.content
            }, {
                removeOnComplete: true,
            });
            console.log(`Job ${job.id} added to queue`);
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

Import the scheduleJobs function into the api/schedule endpoint and trigger the function every minute using Node Cron.

//👉🏻 api/schedule/route.ts
import cron from "node-cron";

export async function POST(req: NextRequest) { 
    const { schedule } = await req.json();
    try {
        await redis.set("schedule", JSON.stringify(schedule));
        cron.schedule('* * * * *', async() => {
            console.log('Triggering jobs...');
            await scheduleJobs(schedule);
        });

        return NextResponse.json(
            { message: "Schedule updated!", schedule },
            { status: 200 }
        );
    } catch (error) { 
        return NextResponse.json(
            { message: "Error updating schedule", error },
            { status: 500 }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, add a worker function within the workers.ts file that executes the jobs within the queue by sending the posts’ content to X (Twitter).

//👇🏻 processing jobs
const scheduleWorker = new Worker('schedule-queue', async (job) => {
  console.log(`Processing job ${job.id} of type ${job.name} with data: ${job.data.message}`)
    console.log("Posting content...")

    //👇🏻 post content to X
    const postTweet = await fetch("https://api.twitter.com/2/tweets", {
        method: "POST",
        headers: {
            "Content-type": "application/json",
            Authorization: `Bearer ${data.accessToken}`,
        },
        body: JSON.stringify({ text: job.data.message })
    });
    if (postTweet.ok) { 
          console.log("Content posted!")
    }
}, { connection: redis})

//👇🏻 listening for completed job
scheduleWorker.on('completed', job => {
    console.log(`${job.id} has completed!`);
});
Enter fullscreen mode Exit fullscreen mode

Finally, you can execute the worker by running npm run worker after updating the scripts within the package.json file.

{
 "scripts": {
    "worker": "npx tsx --watch src/worker.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Congratulations! You’ve completed the project for this tutorial.


Conclusion

So far, you’ve learned how to authenticate users via X, store data in a Redis database, create and manage jobs with Redis and BullMQ, and integrate AI assistants into your Next.js applications using CopilotKit.

CopilotKit is an incredible tool that allows you to add AI Copilots to your products within minutes. Whether you're interested in AI chatbots and assistants or automating complex tasks, simplifies the process.

If you need to build an AI product or integrate an AI tool into your software applications, you should consider CopilotKit.

You can find the source code for this tutorial on GitHub:

https://github.com/dha-stix/ai-post-generator-and-scheduler-with-copilotkit

Thank you for reading!

Top comments (13)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Genuine questions:

Why would you have AI generate your content for you? What possible benefit is there in that for anyone (other than helping to fill the internet with more AI generated junk)?

Also, is 'Coppiotkit on Githhub' the same as 'CopilotKit on Github'? 😜
Generated image fail

Collapse
 
welshapple profile image
William

😱 😱 😱 😱 😱 sounds like someone doesn't want to 10x their posts on twitter 😱 😱 😱 😱 😱

Do you not even AI bro ?

🤖🎃 🤠😈 Here's 3 projects I'm working on to help speed up productivity 🤖🎃 YOU WONT BELIEVE NUMBER 2 (im shoving open AI coppiotkit straight up my ass) 🤖🎃🤖🎃 🤠😈

Collapse
 
jonrandy profile image
Jon Randy 🎖️

😂

Collapse
 
derekmurawsky profile image
Derek Murawsky

I'm torn... This was actually a really good article... but the goal and title, not so much. Why on earth would you want to 10x your posts with crap AI content? That's part of the whole problem with how AI is being used right now.

Collapse
 
trevorbenson profile image
illuminatus • Edited

I think this is a great walkthrough on how to setup and integrate a project with Twitter API and their OAuth 2.

I do have a few comments though.

  1. In the npx create-next-app social-media-scheduler step for Would you like to use /src directory? question you show Yes. Throughout the rest of the example steps this is never used or suggested the user move into src before following additional steps, maybe that was not used in your setup as multiple steps later don't seem to account for a ./src directory. For example:

    • During section The Dashboard Page the user would still inside social-media-scheduler directory:
     cd app
     mkdir dashboard && cd dashboard
     touch page.tsx
    
  • During section Authenticating users via X it also leaves out the ./src directory. However, the user's terminal would still be inside ./src/app/dashboard from prior step of making page.tsx, no step moved back to social-media-scheduler and the start of this section does not suggest starting a terminal from the project root again.

     cd app
     mkdir api && cd api
     mkdir twitter && cd twitter
     touch route.ts
    
  1. During section The Dashboard Page the user is instructed to create a utils file, not a util.ts file:
    • src/app/dashboard/page.tsx: import { availableSchedule } from "../util";
    • src/app/components/App.tsx: import { formatTime, tableHeadings } from "@/app/util";
    • src/app/page.tsx: import { getTwitterOauthUrl } from "@/app/util";

Naming it either utils, or util leads to errors in npm run dev causing the site to not work and no Sign in with Twitter button to be accessible.

I think resolving these very small oversights would allow a complete novice to follow your post, remove any confusions about paths, and any compilation errors they would otherwise run into.

Even with these minor issues I still think its a worthwhile read and a good walk through. Thanks for the article!

Collapse
 
ivictbor profile image
Ivan Borshchov

This is awesome, man!!!

Collapse
 
uliyahoo profile image
uliyahoo

🔥🔥🔥

Collapse
 
james0123 profile image
James

How did you create that cover image?

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Great article Nevo!

Collapse
 
justinwells21 profile image
justinwells21

Really nice tutorial, but don't love the title.

Collapse
 
arindam_1729 profile image
Arindam Majumder

Great One!

Collapse
 
time121212 profile image
tim brandom

Nice one Nevo!