Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
BOT_TOKEN=
GITHUB_ACCESS_TOKEN=
GITHUB_APP_ID=
GUILD_ID=
GITHUB_USERNAME=
GITHUB_REPOSITORY=
GITHUB_REPOSITORY=
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
node_modules/
.env

.yarn/install-state.gz

*.pem
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
511 changes: 511 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

213 changes: 154 additions & 59 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,169 @@
import DiscordJS from "discord.js";
import dotenv from "dotenv";
import { Octokit } from "@octokit/rest";
import { getModal } from "./utils";
import express from "express";
dotenv.config();
import * as DiscordJS from "discord.js"
import dotenv from "dotenv"
import { App } from "octokit"
import { getModal } from "./utils.js"
import arkenv from "arkenv"
import express from "express"
import { regex } from "arktype"
dotenv.config()

const app = express();
const PORT = process.env.PORT || 3000;
const env = arkenv({
"PORT?": "number.port",
GITHUB_APP_ID: "string > 0",
GITHUB_APP_INSTALLATION_ID: "string.integer.parse",
GITHUB_USERNAME: "string > 0",
BOT_TOKEN: "string > 0",
GUILD_ID: "string.integer > 0",
GITHUB_REPOSITORY: [
regex("^(?<owner>[^/]+)/(?<repo>[^/]+)$"),
"=>",
(repoAndOwner) => {
const [owner, repo] = repoAndOwner.split("/", 2)
return { owner, repo }
},
],
})

app.use(express.json());
// const app = express()

app.get("/", (req, res) => {
res.send("Github issues bot!");
});
// app.use(express.json())

app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
// app.get("/", (_, res) => {
// res.send("Github issues bot!")
// })

// app.listen(env.PORT ?? 3000, () => {
// console.log(`Server is listening on port ${env.PORT ?? 3000}`)
// })

const reporter = new App({
appId: env.GITHUB_APP_ID,
privateKey: await Bun.file("./github-app-key.pem").text(),
log: console,
})

const gh = await reporter.getInstallationOctokit(env.GITHUB_APP_INSTALLATION_ID)

const { owner, repo } = env.GITHUB_REPOSITORY

const { data: repository } = await gh.rest.repos.get({ owner, repo })
console.info(`Acting on ${repository.full_name}`)

const client = new DiscordJS.Client({
intents: ["Guilds", "GuildMessages"],
});
intents: ["Guilds", "GuildMessages"],
})

client.on("ready", () => {
console.log("issue bot ready");
const guildId = process.env.GUILD_ID || "";
client.on("clientReady", async () => {
console.log("Bot is ready.")
const guildId = process.env.GUILD_ID || ""

const guild = client.guilds.cache.get(guildId);
const guild = client.guilds.cache.get(guildId)
if (!guild) {
throw new Error("Guild not found")
}

let commands;
const commands = guild.commands

if (guild) {
commands = guild.commands;
} else {
commands = client.application?.commands;
}
for (const [, cmd] of await commands.fetch()) {
await commands.delete(cmd.id)
}

await commands.create({
name: "To Github Bug",
type: 3,
})

await commands.create({
name: "To Github Feature Request",
type: 3,
})

commands?.create({
name: "Open github issue",
type: 3,
});
});
await commands.create({
name: "To Github Task",
type: 3,
})
})

client.on("interactionCreate", async (interaction) => {
if (interaction.isMessageContextMenuCommand()) {
const { commandName, targetMessage } = interaction;
if (commandName === "Open github issue") {
const modal = getModal(targetMessage.content);
interaction.showModal(modal);
}
} else if (interaction.isModalSubmit()) {
const { fields } = interaction;
const issueTitle = fields.getField("issueTitle").value;
const issueDescription = fields.getField("issueDescription").value;
const octokit = new Octokit({
auth: process.env.GITHUB_ACCESS_TOKEN,
baseUrl: "https://api.github.com",
});

octokit.rest.issues
.create({
owner: process.env.GITHUB_USERNAME || "",
repo: process.env.GITHUB_REPOSITORY || "",
title: issueTitle,
body: issueDescription,
})
.then((res) => {
interaction.reply(`Issue created: ${res.data.html_url}`);
});
if (interaction.isMessageContextMenuCommand()) {
const { commandName, targetMessage, user } = interaction
console.log(`Received command ${commandName} from ${user.tag}`)
const githubIssueCommand =
/^To Github (?<type>Bug|Feature Request|Task)$/.exec(commandName)
if (githubIssueCommand) {
const { data: labels } = await gh.rest.issues
.listLabelsForRepo({ owner, repo })
.catch(() => ({ data: [] }))

const { data: issueTypes } = await gh
.request("GET /orgs/{org}/issue-types", { org: owner })
.catch(() => ({ data: [] }))

const { data: milestones } = await gh.rest.issues
.listMilestones({ owner, repo })
.catch(() => ({ data: [] }))

const { data: collaborators } = await gh.rest.repos
.listCollaborators({ owner, repo })
.catch(() => ({ data: [] }))

const modal = getModal({
id: `create github issue ${githubIssueCommand.groups?.type}`,
user,
message: targetMessage,
labels,
issueTypes,
milestones,
collaborators,
})
interaction.showModal(modal)
}
});
} else if (interaction.isModalSubmit()) {
const { fields, customId } = interaction
console.log(
`Received modal submit ${interaction.customId} from ${interaction.user.tag}`
)
const title = fields.getTextInputValue("title")
const body = fields.getTextInputValue("desc")
const labels = fields.fields.has("labels")
? fields.getStringSelectValues("labels")
: []
const [milestone] = fields.fields.has("milestone")
? fields.getStringSelectValues("milestone")
: []
const [assignee] = fields.fields.has("assignee")
? fields.getStringSelectValues("assignee")
: []

const type = {
Task: "Task",
Bug: "Bug",
"Feature Request": "Feature",
// "Dependencies",
}[customId.replace("create github issue ", "")]

console.log(`Creating issue with`, {
title,
body,
labels,
milestone,
assignee,
type,
})

const { data: issue } = await gh.rest.issues.create({
owner,
repo,
title,
body,
milestone,
assignee,
labels: [...labels],
type,
})

await interaction.reply(`[Created #${issue.number}](${issue.html_url})`)
}
})

client.login(process.env.BOT_TOKEN);
client.login(process.env.BOT_TOKEN)
19 changes: 13 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
{
"name": "github-issues-bot",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"author": "Mohd Shamoon",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^18.12.0",
"@types/express": "^4.17.13",
"discord.js": "14.0.0-dev.1650931749-df64d3e",
"dotenv": "^16.0.0",
"express": "^4.18.0",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
"@types/express": "^4.17.25",
"arkenv": "^0.7.8",
"arktype": "^2.1.29",
"discord.js": "14.25.1",
"dotenv": "^16.6.1",
"express": "^5.2.1",
"octokit": "^5.0.5",
"ts-node": "^10.9.2",
"typescript": "^4.9.5"
},
"scripts": {
"start": "ts-node index.ts"
},
"engines": {
"node": "16.13.0"
},
"devDependencies": {
"@types/bun": "^1.3.5"
}
}
6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
Expand All @@ -24,9 +24,9 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */

/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"module": "nodenext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
Expand Down
Loading