Skip to content

Commit 867f60c

Browse files
authored
Merge pull request #15 from coder/productboard
ProductBoard
2 parents 09099a2 + d595825 commit 867f60c

File tree

12 files changed

+134
-12
lines changed

12 files changed

+134
-12
lines changed

bun.lockb

-15.2 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@uwu/configmasher": "latest",
2424
"discord.js": "^14.15.3",
25-
"octokit": "^4.0.2"
25+
"ofetch": "^1.4.1"
2626
},
2727
"trustedDependencies": [
2828
"@biomejs/biome"

src/commands/index.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,34 @@
1-
export { default as close } from "./util/close.js";
2-
export { default as reopen } from "./util/reopen.js";
1+
import type {
2+
SlashCommandBuilder,
3+
ChatInputCommandInteraction,
4+
ContextMenuCommandBuilder,
5+
ContextMenuCommandInteraction,
6+
SlashCommandOptionsOnlyBuilder,
7+
} from "discord.js";
38

4-
export { default as walkthrough } from "./util/walkthrough.js";
9+
import { default as product_notes } from "./product/notes.js";
10+
11+
import { default as close } from "./util/close.js";
12+
import { default as reopen } from "./util/reopen.js";
13+
import { default as walkthrough } from "./util/walkthrough.js";
14+
15+
type AnyCommandBuilder =
16+
| SlashCommandBuilder
17+
| SlashCommandOptionsOnlyBuilder
18+
| ContextMenuCommandBuilder;
19+
type AnyInteraction =
20+
| ChatInputCommandInteraction
21+
| ContextMenuCommandInteraction;
22+
23+
const commandObject: {
24+
[key: string]: {
25+
data: AnyCommandBuilder;
26+
execute: (interaction: AnyInteraction) => unknown;
27+
};
28+
} = {};
29+
30+
for (const command of [product_notes, close, reopen, walkthrough]) {
31+
commandObject[command.data.name] = command;
32+
}
33+
34+
export default commandObject;

src/commands/product/notes.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { config } from "@lib/config.js";
2+
import { makeCodeBlock } from "@lib/discord/messages.js";
3+
4+
import { ofetch } from "ofetch";
5+
6+
import {
7+
ContextMenuCommandBuilder,
8+
ApplicationCommandType,
9+
type MessageContextMenuCommandInteraction,
10+
MessageFlags,
11+
ActionRowBuilder,
12+
ButtonStyle,
13+
ButtonBuilder,
14+
} from "discord.js";
15+
16+
// TODO: try to make the official API package work
17+
async function createNote(body) {
18+
return ofetch("https://api.productboard.com/notes", {
19+
method: "POST",
20+
headers: {
21+
Authorization: `Bearer ${config.productBoard.token}`,
22+
},
23+
body,
24+
});
25+
}
26+
27+
export default {
28+
data: new ContextMenuCommandBuilder()
29+
.setName("Add to product notes")
30+
.setType(ApplicationCommandType.Message),
31+
32+
execute: async (interaction: MessageContextMenuCommandInteraction) => {
33+
const data = await createNote({
34+
title: `Discord message from ${interaction.targetMessage.author.displayName} (in '${interaction.channel.name}')`, // this will only work for threads
35+
display_url: interaction.targetMessage.url,
36+
content: interaction.targetMessage.content,
37+
38+
company: { id: config.productBoard.companyId },
39+
user: { external_id: `discord:${interaction.targetMessage.author.id}` },
40+
41+
source: { origin: "discord", record_id: interaction.targetId },
42+
});
43+
44+
const replyComponents = [];
45+
46+
if (data.links?.html) {
47+
const button = new ButtonBuilder()
48+
.setLabel("Open in ProductBoard")
49+
.setStyle(ButtonStyle.Link)
50+
.setURL(data.links.html);
51+
52+
replyComponents.push(
53+
new ActionRowBuilder<ButtonBuilder>().addComponents(button),
54+
);
55+
}
56+
57+
await interaction.reply({
58+
content: makeCodeBlock(JSON.stringify(data), "json"),
59+
60+
components: replyComponents,
61+
62+
flags: MessageFlags.Ephemeral,
63+
});
64+
},
65+
};

src/commands/util/close.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
canMemberInteractWithThread,
55
getChannelFromInteraction,
66
isHelpPost,
7-
} from "@lib/channels.js";
7+
} from "@lib/discord/channels.js";
88

99
import {
1010
type ThreadChannel,

src/commands/util/walkthrough.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { config } from "@lib/config.js";
22

3-
import { isHelpPost as isHelpThread } from "@lib/channels.js";
3+
import { isHelpPost as isHelpThread } from "@lib/discord/channels.js";
44
import issueCategorySelector from "@components/issueCategorySelector.js";
55

66
import {

src/deploy-commands.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { config } from "@lib/config.js";
2-
import * as commands from "@commands/index.js";
2+
import { getClientIDFromToken } from "@lib/discord/users.js";
3+
4+
import commands from "@commands/index.js";
35

46
import { REST, Routes } from "discord.js";
57

68
// Construct and prepare an instance of the REST module
79
const rest = new REST().setToken(config.token);
810

9-
const commandData = Object.values(commands).map((command) => command.data);
11+
const commandData = Object.values(commands).map((command) =>
12+
command.data.toJSON(),
13+
);
1014

1115
console.log(
1216
`Started refreshing ${commandData.length} application (/) commands.`,
@@ -15,7 +19,10 @@ console.log(
1519
// The put method is used to fully refresh all commands in the guild with the current set
1620
// biome-ignore lint/suspicious/noExplicitAny: TODO: need to figure out the proper type
1721
const data: any = await rest.put(
18-
Routes.applicationGuildCommands("1063886601165471814", config.serverId), // TODO: guess client ID from token
22+
Routes.applicationGuildCommands(
23+
getClientIDFromToken(config.token),
24+
config.serverId,
25+
),
1926
{ body: commandData },
2027
);
2128

src/events/commands.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import * as commands from "@commands/index.js";
1+
import commands from "@commands/index.js";
22

33
import { type Client, Events } from "discord.js";
44

55
export default function registerEvents(client: Client) {
66
return client.on(Events.InteractionCreate, async (interaction) => {
7-
if (interaction.isChatInputCommand()) {
7+
if (
8+
interaction.isChatInputCommand() ||
9+
interaction.isMessageContextMenuCommand()
10+
) {
811
const command = commands[interaction.commandName];
912

1013
if (!command) {
1114
console.error(
12-
`No command matching ${interaction.commandName} was found.`,
15+
`No command matching "${interaction.commandName}" was found.`,
1316
);
1417
return;
1518
}
@@ -20,6 +23,7 @@ export default function registerEvents(client: Client) {
2023
console.error(error);
2124

2225
// TODO: make generic replyOrFollowUp method
26+
// TODO: log error if the user is admin
2327
if (interaction.replied || interaction.deferred) {
2428
await interaction.followUp({
2529
content: "There was an error while executing this command!",

src/lib/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ interface Config {
2020
vscode: string;
2121
};
2222

23+
productBoard: {
24+
token: string;
25+
companyId: string;
26+
};
27+
2328
presenceDelay: number;
2429
}
2530

@@ -50,5 +55,8 @@ export const { config, layers } = await loadConfig<Config>({
5055
["emojis", "macos"],
5156
["emojis", "windows"],
5257
["emojis", "vscode"],
58+
59+
["productBoard", "token"],
60+
["productBoard", "companyId"],
5361
],
5462
});
File renamed without changes.

src/lib/discord/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function makeCodeBlock(text, language = "") {
2+
return `\`\`\`${language}
3+
${text}
4+
\`\`\``;
5+
}

src/lib/discord/users.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function getClientIDFromToken(token: string): string {
2+
return atob(token.split(".")[0]);
3+
}

0 commit comments

Comments
 (0)