How to build your own GitHub roast app
A few weeks ago, I built a GitHub roast app that kind of blew up, and today I am finally writing about how I built it.
But first, how does the application work?
The flow is simple - you enter your GitHub username, and OpenAI generates a roast based on your public repositories
and number of followers
.
The prompt we send to OpenAI to generate the roast๐
const messages = [
{ role: "system", content: "You are a witty assistant asked to create a light-hearted roast." },
{ role: "user", content: `Tell me a roast about a GitHub user named ${profileData.name || username} who has ${profileData.public_repos} repositories and ${profileData.followers} followers.` },
];
My biggest takeaway from this fun weekend project was, that you do not build a fancy UI to have users. Easy onboarding is the key!
Here's some screenshots attached as proof:
Note: The GitHub authentication was added 12 hours after I posted this project online, so my guess is, there were at least 500+ more users, since I had to renew my OpenAI twice before this.
Get started building
If you know basic HTML, CSS, JavaScript and know how to work with APIs, this walk through will be easy to follow.
Before diving right into the code, let's understand the parts we have in this application:
Login page- You need to login through GitHub, this was added later to prevent the GitHub API from exceeding. You can read the documentation for reference.
Homepage - A simple UI built with HTML and CSS, where you enter your GitHub username and it generates a roast for you by fetching profile details from the GitHub API and prompting the OpenAI API to generate a roast.
A brief architecture diagram of the application:
Code breakdown
You can probably build a better UI than me, so I'll skip the HTML part. If you need to check it out, I've linked the repository below.
Now, let's dive into how we handle GitHub authentication and generate the roast ๐
Code to generate the roast and save the roast as image to share on social media:
In this code, we have two functions:
The
getRoast
function retrieves a roast associated with a GitHub username from a server. It checks if a username is entered, requests the roast, and displays it on the webpage; if unsuccessful, it informs the user of the failure.The
saveRoast
function allows the user to save this roast as an image by capturing the part of the webpage showing the roast and initiating a download of the image.
async function getRoast() {
const username = document.getElementById("username").value;
if (!username) {
alert("Please enter a GitHub username.");
return;
}
try {
const response = await fetch(`/roast/${username}`);
const data = await response.json();
document.getElementById("roastOutput").textContent =
data.roast || "No roast available.";
} catch (error) {
console.error("Failed to fetch roast:", error);
document.getElementById("roastOutput").textContent =
"Failed to fetch roast. Please try again.";
}
}
function saveRoast() {
html2canvas(document.getElementById("roastWrapper")).then(function (canvas) {
var link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = "roast.png";
link.click();
});
}
Code that handles the GitHub login and uses the GitHub and OpenAI API to generate the roast
In this block, we set up a web server that handles user logins through GitHub and generates roasts about GitHub users. It uses Express for server operations and Passport.js for GitHub authentication. Users are redirected to log in via GitHub, and once authenticated, can access a homepage. The server also includes an endpoint that uses the OpenAI API to create roasts based on a user's GitHub profile. It handles errors gracefully and logs server activity.
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const passport = require('passport');
const GitHubStrategy = require('passport-github').Strategy;
const session = require('express-session');
const path = require('path');
const OpenAI = require('openai');
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.use(session({
secret: 'replace_this_with_a_secure_secret',
resave: false,
saveUninitialized: true,
}));
// Passport setup
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: "https://github-roast.up.railway.app/auth/github/callback"
}, (accessToken, refreshToken, profile, done) => done(null, profile)));
app.use(passport.initialize());
app.use(passport.session());
// Route to serve the login page
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.redirect('/home');
} else {
res.sendFile(path.join(__dirname, 'public', 'login.html'));
}
});
// Route to handle GitHub authentication
app.get('/auth/github', passport.authenticate('github'));
// GitHub callback route
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/' }),
(req, res) => res.redirect('/home'));
// Protected route to serve the homepage after login
app.get('/home', (req, res) => {
if (req.isAuthenticated()) {
res.sendFile(path.join(__dirname, 'public', 'home.html'));
} else {
res.redirect('/');
}
});
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY_NEW4,
});
// Endpoint to generate a roast
app.get('/roast/:username', async (req, res) => {
const { username } = req.params;
try {
const githubResponse = await axios.get(`https://api.github.com/users/${username}`);
const profileData = githubResponse.data;
if (!profileData) return res.status(404).json({ error: "GitHub user not found" });
const messages = [
{ role: "system", content: "You are a witty assistant asked to create a light-hearted roast." },
{ role: "user", content: `Tell me a roast about a GitHub user named ${profileData.name || username} who has ${profileData.public_repos} repositories and ${profileData.followers} followers.` },
];
const completion = await openai.chat.completions.create({
messages: messages,
model: "gpt-3.5-turbo",
});
if (completion.choices && completion.choices.length > 0) {
const roast = completion.choices[0].message.content;
res.json({ roast });
} else {
res.status(500).json({ error: "Failed to generate a roast from AI" });
}
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Failed to fetch data or generate roast' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server is running on http://localhost:${PORT}`));
And that's a wrap! You can find the code on GitHub. If you liked it, feel free to star the repo. If you would like to share more cool project ideas, feel free to DM me @HaimantikaM.