A Discord bot to expand issue links to a private GitHub repository
June 30, 2023 ยท View on GitHub
I have a private Discord channel and a private GitHub repository.
Every time I paste a URL to an issue in the private GitHub repository I want it to be expanded in the channel.
I ended up solving this by running a custom Discord bot on Glitch.
Why Glitch?
I chose Glitch because I needed convenient hosting where the bot process would be running all the time. Discord bots need to stay connected to Discord via a WebSocket, so hosting that scales-to-zero won't work.
I already have a paid Glitch account that lets me "boost" up to five apps - where boosting keeps the apps running all the time. So I decided to use that.
A Discord bot token
My bot needs a token for talking to Discord and one for talking to GitHub.
I created the Discord one by following the README in this discord-bot-example by Dan Reeves on Glitch.
I created a new Discord app at https://discordapp.com/developers/applications/me
The token I needed was on the "Bot" page:
I clicked "Reset token" and copied out the token, to use later.
I also needed to turn on "message content intent" for my bot, further down that page:
I needed the "application ID" from the "General Information" page of the bot application. I used that to construct this URL:
https://discordapp.com/oauth2/authorize?&client_id=<APPLICATION_ID>&scope=bot&permissions=0
Then I visited that page and used it to add the bot to my Datasette Discord server.
Adding the bot to a channel
I'm still not entirely sure the right way to do this. Clicking "Create invite" in the channel menu doesn't seem to allow a bot to be invited. The only thing that definitely works is scrolling to the very top of a channel and clicking the "Add members or roles" button:
That provided a menu that allowed me to invite my bot.
A GitHub personal access token
I needed a GitHub API token that could access the issues API for my private repository.
First I tried using the new ability in GitHub to create scoped access tokens that could only access one repo. This seemed to work... but I later found out that my regular GitHub account is highly rate-limited and so that token wasn't able to make very many requests.
Instead, I created a brand new GitHub user account called datasette-github-bot. I invited this to my private repository, then created a personal access token for it with permission to read repository data.
Running the bot on Glitch
I started by remixing the discord-bot-example project. I added my two API keys to the .env panel there:
I decided to use node-fetch to access the GitHub API. It turns out I needed to use version 2 of that on Glitch, since version 3 uses ES Modules which I don't think are supported yet (or maybe I'm on an older Node version on Glitch?)
I put this in my package.json on Glitch:
{
"name": "discord-1337-bot",
"version": "0.0.1",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"eris": "^0.16.1",
"node-fetch": "2.6.7"
},
"engines": {
"node": "16"
}
}
Then I iterated my way towards this code in server.js, with a bit of help from ChatGPT 4:
const Eris = require("eris");
const fetch = require('node-fetch');
function extractIssueId(input) {
const match = input.match(
/https:\/\/github\.com\/simonw\/my-private-repo\/issues\/(\d+)/
);
return match ? match[1] : null;
}
async function fetchIssue(id) {
const response = await fetch(
"https://api.github.com/repos/simonw/my-private-repo/issues/" + id,
{
headers: {
Authorization: "token " + process.env.GITHUB_PAT_TOKEN,
},
}
);
if (!response.ok) {
let text = await response.text();
return { error: `HTTP error! status: ${response.status} ${text}` };
}
return await response.json();
}
const bot = new Eris(process.env.DISCORD_BOT_TOKEN);
bot.on("ready", () => {
console.log("Ready!");
console.log(bot.user.id);
});
bot.on("messageCreate", async (msg) => {
console.log(msg.channel.name, msg.author.username, msg.content);
if (msg.channel.name == "my-private-channel" && msg.author.id != bot.user.id) {
let id = extractIssueId(msg.content);
console.log('issue ID', id);
if (id) {
const issue = await fetchIssue(id);
console.log(issue);
if (!issue.error) {
bot.createMessage(msg.channel.id, `${issue.number}: ${issue.title} - ${issue.html_url}`);
}
}
}
});
bot.connect();
The great thing about Glitch is the server automatically restarted every time I edited the file in my browser.
And... this works!
Now every time I paste a URL to https://github.com/simonw/my-private-repo/issues/361 into the #my-private-channel channel, the bot responds with the title and URL for that issue:
361: Discord bot to expand issue links - https://github.com/simonw/my-private-repo/issues/361
And since this whole system is extremely easy to hack on, adding additional features should be very straight-forward.
Avoiding an infinite loop
I did have one nasty bug while I was putting this together. The fix was this:
if (msg.channel.name == "my-private-channel" && msg.author.id != bot.user.id) {
That && msg.author.id != bot.user.id bit is crucial. Before I added that, any time I pasted a URL into the channel the bot would reply... and then it would see its own message and reply over and over again in an infinite loop!
Thankfully since I was running on Glitch I saw what happened and quickly commented out the bot.createMessage() line to stop the loop.