Skip to content

Commit 2f622c0

Browse files
committed
move to bun, implement labels,types,milestones,assignees, upgrade deps, use github apps, etc
1 parent aae90e9 commit 2f622c0

File tree

8 files changed

+823
-1301
lines changed

8 files changed

+823
-1301
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
BOT_TOKEN=
2-
GITHUB_ACCESS_TOKEN=
2+
GITHUB_APP_ID=
33
GUILD_ID=
44
GITHUB_USERNAME=
5-
GITHUB_REPOSITORY=
5+
GITHUB_REPOSITORY=

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ node_modules/
22
.env
33

44
.yarn/install-state.gz
5+
6+
*.pem

bun.lock

Lines changed: 511 additions & 0 deletions
Large diffs are not rendered by default.

index.ts

Lines changed: 133 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,168 @@
1-
import DiscordJS from "discord.js"
1+
import * as DiscordJS from "discord.js"
22
import dotenv from "dotenv"
3-
import { Octokit } from "@octokit/rest"
4-
import { getModal } from "./utils"
3+
import { App } from "octokit"
4+
import { getModal } from "./utils.js"
5+
import arkenv from "arkenv"
56
import express from "express"
7+
import { regex } from "arktype"
68
dotenv.config()
79

8-
const app = express()
9-
const PORT = process.env.PORT || 3000
10+
const env = arkenv({
11+
"PORT?": "number.port",
12+
GITHUB_APP_ID: "string > 0",
13+
GITHUB_APP_INSTALLATION_ID: "string.integer.parse",
14+
GITHUB_USERNAME: "string > 0",
15+
BOT_TOKEN: "string > 0",
16+
GUILD_ID: "string.integer > 0",
17+
GITHUB_REPOSITORY: [
18+
regex("^(?<owner>[^/]+)/(?<repo>[^/]+)$"),
19+
"=>",
20+
(repoAndOwner) => {
21+
const [owner, repo] = repoAndOwner.split("/", 2)
22+
return { owner, repo }
23+
},
24+
],
25+
})
1026

11-
app.use(express.json())
27+
// const app = express()
1228

13-
app.get("/", (req, res) => {
14-
res.send("Github issues bot!")
15-
})
29+
// app.use(express.json())
30+
31+
// app.get("/", (_, res) => {
32+
// res.send("Github issues bot!")
33+
// })
1634

17-
app.listen(PORT, () => {
18-
console.log(`Server is listening on port ${PORT}`)
35+
// app.listen(env.PORT ?? 3000, () => {
36+
// console.log(`Server is listening on port ${env.PORT ?? 3000}`)
37+
// })
38+
39+
const reporter = new App({
40+
appId: env.GITHUB_APP_ID,
41+
privateKey: await Bun.file("./github-app-key.pem").text(),
42+
log: console,
1943
})
2044

45+
const gh = await reporter.getInstallationOctokit(env.GITHUB_APP_INSTALLATION_ID)
46+
47+
const { owner, repo } = env.GITHUB_REPOSITORY
48+
49+
const { data: repository } = await gh.rest.repos.get({ owner, repo })
50+
console.info(`Acting on ${repository.full_name}`)
51+
2152
const client = new DiscordJS.Client({
2253
intents: ["Guilds", "GuildMessages"],
2354
})
2455

25-
client.on("ready", () => {
26-
console.log("issue bot ready")
56+
client.on("clientReady", async () => {
57+
console.log("Bot is ready.")
2758
const guildId = process.env.GUILD_ID || ""
2859

2960
const guild = client.guilds.cache.get(guildId)
61+
if (!guild) {
62+
throw new Error("Guild not found")
63+
}
3064

31-
let commands:
32-
| DiscordJS.GuildApplicationCommandManager
33-
| DiscordJS.ApplicationCommandManager
34-
| undefined
65+
const commands = guild.commands
3566

36-
if (guild) {
37-
commands = guild.commands
38-
} else {
39-
commands = client.application?.commands
67+
for (const [, cmd] of await commands.fetch()) {
68+
await commands.delete(cmd.id)
4069
}
4170

42-
commands?.create({
43-
name: "Open github issue",
71+
await commands.create({
72+
name: "To Github Bug",
73+
type: 3,
74+
})
75+
76+
await commands.create({
77+
name: "To Github Feature Request",
78+
type: 3,
79+
})
80+
81+
await commands.create({
82+
name: "To Github Task",
4483
type: 3,
4584
})
4685
})
4786

4887
client.on("interactionCreate", async (interaction) => {
4988
if (interaction.isMessageContextMenuCommand()) {
50-
const { commandName, targetMessage } = interaction
51-
if (commandName === "Open github issue") {
52-
const modal = getModal(targetMessage)
89+
const { commandName, targetMessage, user } = interaction
90+
console.log(`Received command ${commandName} from ${user.tag}`)
91+
const githubIssueCommand =
92+
/^To Github (?<type>Bug|Feature Request|Task)$/.exec(commandName)
93+
if (githubIssueCommand) {
94+
const { data: labels } = await gh.rest.issues
95+
.listLabelsForRepo({ owner, repo })
96+
.catch(() => ({ data: [] }))
97+
98+
const { data: issueTypes } = await gh
99+
.request("GET /orgs/{org}/issue-types", { org: owner })
100+
.catch(() => ({ data: [] }))
101+
102+
const { data: milestones } = await gh.rest.issues
103+
.listMilestones({ owner, repo })
104+
.catch(() => ({ data: [] }))
105+
106+
const { data: collaborators } = await gh.rest.repos
107+
.listCollaborators({ owner, repo })
108+
.catch(() => ({ data: [] }))
109+
110+
const modal = getModal({
111+
id: `create github issue ${githubIssueCommand.groups?.type}`,
112+
user,
113+
message: targetMessage,
114+
labels,
115+
issueTypes,
116+
milestones,
117+
collaborators,
118+
})
53119
interaction.showModal(modal)
54120
}
55121
} else if (interaction.isModalSubmit()) {
56-
const { fields } = interaction
57-
const title = fields.getTextInputValue("issueTitle")
58-
const body = fields.getTextInputValue("issueDescription")
59-
const octokit = new Octokit({
60-
auth: process.env.GITHUB_ACCESS_TOKEN,
61-
baseUrl: "https://api.github.com",
62-
})
122+
const { fields, customId } = interaction
123+
console.log(
124+
`Received modal submit ${interaction.customId} from ${interaction.user.tag}`
125+
)
126+
const title = fields.getTextInputValue("title")
127+
const body = fields.getTextInputValue("desc")
128+
const labels = fields.fields.has("labels")
129+
? fields.getStringSelectValues("labels")
130+
: []
131+
const [milestone] = fields.fields.has("milestone")
132+
? fields.getStringSelectValues("milestone")
133+
: []
134+
const [assignee] = fields.fields.has("assignee")
135+
? fields.getStringSelectValues("assignee")
136+
: []
63137

64-
let [owner, repo] = (process.env.GITHUB_REPOSITORY || "").split("/", 2)
65-
if (!repo) {
66-
repo = owner
67-
owner = process.env.GITHUB_USERNAME || ""
68-
}
138+
const type = {
139+
Task: "Task",
140+
Bug: "Bug",
141+
"Feature Request": "Feature",
142+
// "Dependencies",
143+
}[customId.replace("create github issue ", "")]
69144

70-
octokit.rest.issues.create({ owner, repo, title, body }).then((res) => {
71-
interaction.reply(`Issue created: ${res.data.html_url}`)
145+
console.log(`Creating issue with`, {
146+
title,
147+
body,
148+
labels,
149+
milestone,
150+
assignee,
151+
type,
72152
})
153+
154+
const { data: issue } = await gh.rest.issues.create({
155+
owner,
156+
repo,
157+
title,
158+
body,
159+
milestone,
160+
assignee,
161+
labels: [...labels],
162+
type,
163+
})
164+
165+
await interaction.reply(`[Created #${issue.number}](${issue.html_url})`)
73166
}
74167
})
75168

package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
{
22
"name": "github-issues-bot",
33
"version": "1.0.0",
4+
"type": "module",
45
"main": "index.js",
56
"author": "Mohd Shamoon",
67
"license": "MIT",
78
"dependencies": {
89
"@octokit/rest": "^18.12.0",
9-
"@types/express": "^4.17.13",
10+
"@types/express": "^4.17.25",
11+
"arkenv": "^0.7.8",
12+
"arktype": "^2.1.29",
1013
"discord.js": "14.25.1",
11-
"dotenv": "^16.0.0",
12-
"express": "^4.18.0",
13-
"ts-node": "^10.7.0",
14-
"typescript": "^4.6.3"
14+
"dotenv": "^16.6.1",
15+
"express": "^5.2.1",
16+
"octokit": "^5.0.5",
17+
"ts-node": "^10.9.2",
18+
"typescript": "^4.9.5"
1519
},
1620
"scripts": {
1721
"start": "ts-node index.ts"
1822
},
1923
"engines": {
2024
"node": "16.13.0"
25+
},
26+
"devDependencies": {
27+
"@types/bun": "^1.3.5"
2128
}
2229
}

tsconfig.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
1212

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

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

0 commit comments

Comments
 (0)