Build an AI-powered Social Media Post Scheduler (Twitter API, Next.js & Copilotkit)
David Asaolu
Posted on July 17, 2024
TL;DR
In this tutorial, you will learn how to build an AI-powered social media content generator and scheduler that allows you to schedule posts and generate content effectively.
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, master social media APIs and to develop high IQ social media game.
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 🤖
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
Select your preferred configuration settings. For this tutorial, we'll be using TypeScript and Next.js App Router.
Next, install the project dependencies:
npm install @headlessui/react lodash bullmq ioredis node-cron
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
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>
);
}
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.
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[][];
}
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
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} />
);
}
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: [[], [], [], [], [], [], []],
},
];
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.
Enter the project name and provide answers to the required questions to create a new project and an app.
Set up the user authentication settings to allow you read and write posts on behalf of the users.
Finally, fill the App info
section accordingly.
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>
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
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
- 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.
- The
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 });
}
}
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]);
Congratulations! When users click the Sign in with Twitter
button, it redirects them to the Twitter authorisation page to enable them access the application.
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.
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
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
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}));
}
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>
);
}
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.
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}!`);
},
});
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
});
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.",
});
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...",
});
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 anupdate_schedule
object with atime
andschedule
property. Theschedule
object includescontent
,minutes
,published
, andday
attributes. - The
handler
function describes the action to be carried out when triggered. In the example above, thehandler
function opens theAddPost
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
Once the installation process is complete, you can test your Redis server by running the following code snippet in your terminal:
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 }
);
}
}
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 }
);
}
}
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 }
);
}
}
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 });
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`);
});
}
};
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 }
);
}
}
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!`);
});
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"
}
}
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!
Posted on July 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.