TL;DR π₯
In this tutorial, you'll learn how to build a tweet scheduler. Enter your Twitter credentials and select a time and a date and your tweet will be tweeted at that time.
Rocketgraph: An open-source replacement to Firebase π
Just a quick background about us. Rocketgraph lets developers build web applications in minutes.
We do this by providing:
- PostgresDB hosted on AWS RDS
- Hasura console to GraphQLise your PostgresDB, manage your permissions, crons, schedulers and a lot more.
- Built-in authentication that supports email/password, social, magic link and OTP
- Serverless functions for any additional backend code.
Before we start
We are going to be using Hasura on-off schedulers to trigger Twitter API at the appropriate time and Rocketgraph's edge functions to setup the backend. So we need to create a project on Rocketgraph. It's free.
Signup
And create a project
It will setup a Hasura console. For now you don't need to know how to use it. Since we will be using it via the Hasura API.
Let's set it up π
We'll be using React.js and shadcn UI for the front-end and Node.js for our serverless functions.
create a folder for project, client and server:
mkdir twitter-app && cd twitter-app
mkdir client server
Setting up Lambda functions
With Rocketgraph you can easily create lambda functions that act as your backend. To setup, note the project name from your project console on Rocketgraph:
npm init -y
npm install express body-parser axios axios-oauth-1.0a
npm install @codegenie/serverless-express
The last install is for express on the AWS Lambdas. You don't need to worry about that as Rocketgraph takes care of Lambda deployments.
Create a file index.js
and add the following code
const serverlessExpress = require('@codegenie/serverless-express')
const app = require('./app')
exports.handler = serverlessExpress({ app })
Now create a file named app.js and add the following code:
// jshint esversion: 6
const express = require("express");
const bodyParser = require("body-parser");
const axios = require("axios");
const addOAuthInterceptor = require("axios-oauth-1.0a");
const app = express();
const port = 3000
app.use(express.json());
const APP_URL = "https://guhys2s4uv24wbh6zoxl3by4xa0im.lambda-url.us-east-2.on.aws/tweet";
app.post("/tweet", function (req, res) {
// Create a client whose requests will be signed
const client = axios.create();
// Specify the OAuth options
const payload = req.body.payload;
const options = {
algorithm: 'HMAC-SHA1',
key: `${payload.key}`,
secret: `${payload.secret}`,
token: `${payload.token}`,
tokenSecret: `${payload.tokenSecret}`,
};
const data = {
"text": `${payload.tweet}`
};
console.log("addOAuthInterceptor is not a function: ", addOAuthInterceptor)
// Add interceptor that signs requests
addOAuthInterceptor.default(client, options);
console.log("params: ", req.body);
client.post("https://api.twitter.com/2/tweets",
data)
.then(response => {
console.log("resp: ", response, req.body.key);
res.status(200).send(`<h2> the result is : Tweeted </h2>` );
})
.catch(err => {
console.log(err);
res.status(500).send(`<h2> the result is : Error </h2>` );
});
});
app.post("/schedule-tweet", function (req, res) {
const body = JSON.parse(req.body);
console.log("body", body)
const jsonb = {
"type": "create_scheduled_event",
"args": {
"webhook": `${APP_URL}`,
"schedule_at": `${body.schedule_at}`,
"include_in_metadata": true,
"payload": {
"tweet": `${body.tweet}`,
"algorithm": 'HMAC-SHA1',
"key": `${body.key}`,
"secret": `${body.secret}`,
"token": `${body.token}`,
"tokenSecret": `${body.tokenSecret}`,
},
"comment": "sample scheduled event comment"
}
}
console.log(req.body);
axios.post("https://hasura-vlklxvo.rocketgraph.app/v1/metadata", jsonb, {
"headers" : {
"Content-Type": "application/json",
"X-Hasura-Role":"admin",
"X-hasura-admin-secret":"********"
},
}).then(function (response) {
console.log("response: ", response);
res.status(200).send({
message: "Your result is tweeted"
})
}).catch(function (error) {
console.log("Error from Hasura: ", error);
res.status(500).send(error)
});
})
app.listen(port, () => console.log(`calculator listening on port ${port}!`))
module.exports = app;
Notice the APP_URL
? Leave it empty for now. We will get this URL once we deploy the function to prod. So now let's go ahead and do that.
Also replace X-hasura-admin-secret
and HASURA_URL with the ones shown on your project's Hasura tab
Deploy the function βοΈ
Rgraph provides a cute CLI to assist you to develop your functions to AWS lambda:
npm install -g rgraph@0.1.10
And with your project name, execute this to deploy your function:
rgraph deploy <function name>
Build the frontend πΌ
Create project
cd client
npx create-next-app@latest my-app --typescript --tailwind --eslint
Add shadcn ui
npx shadcn-ui@latest init
Add necessary components for our app
npx shadcn-ui@latest add button popover calendar card input label
Create UI
In src/app/page.tsx
"use client"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import React from "react"
import { useState } from "react"
import { DatePickerDemo } from "./date"
type ScheduleTweetInput = {
key: String,
secret: String,
token: String,
tokenSecret: String,
tweet: String,
date: any,
time: any,
}
const sendTweet = async (twitterData: ScheduleTweetInput) => {
const { date, time, tweet, key, secret, token, tokenSecret } = twitterData;
console.log(time.split(":"))
date.setHours(...time.split(":"))
console.log(twitterData, date);
const isoDate = date.toISOString()
console.log(isoDate);
const url = "https://guhys2s4uv24wbh6zoxl3by4xa0imsdb.lambda-url.us-east-2.on.aws/schedule-tweet"
const postData = {
"tweet": tweet,
"schedule_at": isoDate,
"key": key,
"secret": secret,
"token": token,
"tokenSecret": tokenSecret
}
let headersList = {
"Accept": "*/*",
"User-Agent": "Thunder Client (https://www.thunderclient.com)",
"Content-Type": "application/json"
}
let bodyContent = JSON.stringify(postData);
let response = await fetch(url, {
method: "POST",
body: bodyContent,
headers: headersList,
mode: "no-cors"
});
let data = await response.text();
console.log(data, response.status, response.json);
console.log("Status", response.status, response);
}
export default function DemoCreateAccount() {
const [key, setKey] = useState("");
const [token, setToken] = useState("");
const [tokenSecret, setTokenSecret] = useState("");
const [tweet, setTweet] = useState("");
const [secret, setSecret] = useState("");
const [date, setDate] = React.useState<Date>()
const [time, setTime] = useState<any>('10:00');
console.log(key, "key")
return (
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-2xl">Twitter scheduler</CardTitle>
<CardDescription>
Enter your credentials below to schedule a tweet
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">API Key</Label>
<Input id="email" type="email" placeholder="API Key" onChange={(e) => setKey(e.target.value)}/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">API Secret</Label>
<Input id="email" type="email" placeholder="API Secret" onChange={(e) => setSecret(e.target.value)}/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Access Token</Label>
<Input id="email" type="email" placeholder="Access Token" onChange={(e) => setToken(e.target.value)} />
</div>
<div className="grid gap-2">
<Label htmlFor="email">Access Secret</Label>
<Input id="email" type="email" placeholder="Access Secret" onChange={(e) => setTokenSecret(e.target.value)}/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Tweet</Label>
<Input id="email" type="email" placeholder="Tweet your tweet here" onChange={(e) => setTweet(e.target.value)}/>
</div>
{DatePickerDemo(setDate, date)}
<input aria-label="Time" type="time" onChange={(e) => setTime(e.target.value)}/>
</CardContent>
<CardFooter>
<Button className="w-full" onClick={() => sendTweet({
key,
token,
tokenSecret,
date,
time,
tweet,
secret,
})}>Schedule tweet</Button>
</CardFooter>
</Card>
)
}
Create a file called src/app/date.tsx
"use client"
import * as React from "react"
import { CalendarIcon } from "@radix-ui/react-icons"
import { format } from "date-fns"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
export function DatePickerDemo(onChange: any, date: any) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[240px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={onChange}
initialFocus
/>
</PopoverContent>
</Popover>
)
}
Deploy
And deploy the front-end on Vercel. Hooray, your app is live π₯³
Top comments (3)
Great article. Well explained too! π
Thank you. You might want to checkout Rocketgraph. If you have any trouble using it, please reach out to me at kaushik@rocketgraph.io and I will give you a lifetime discount on our platform.
I will be sure to check it out.