feat: ping command, github command

This commit is contained in:
xHyroM 2022-07-10 21:41:00 +02:00
parent 06c5155164
commit d89b80c60d
13 changed files with 122 additions and 19 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
# Edit at https://www.toptal.com/developers/gitignore?templates=node # Edit at https://www.toptal.com/developers/gitignore?templates=node
files/config.toml files/config.toml
requests.rest
### Node ### ### Node ###
# Logs # Logs

BIN
bun.lockb

Binary file not shown.

View file

@ -7,3 +7,9 @@ port = 3000
public_key = "CLIENT PUBLIC KEY" public_key = "CLIENT PUBLIC KEY"
token = "CLIENT TOKEN" token = "CLIENT TOKEN"
id = "CLIENT USER ID" id = "CLIENT USER ID"
[commands]
guild_id = "GUILD ID"
[api]
github_personal_access_token = ""

7
files/utilities.toml Normal file
View file

@ -0,0 +1,7 @@
[github]
repositories = [
"oven-sh/bun",
"oven-sh/docs",
"Jarred-Sumner/bun-dependencies",
"xHyroM/bun-discord-bot"
]

View file

@ -2,7 +2,8 @@
"version": "0.0.0", "version": "0.0.0",
"name": "bun-discord-bot", "name": "bun-discord-bot",
"scripts": { "scripts": {
"start": "bun src/index.ts" "start": "bun src/index.ts",
"cloudflare:tunnel": "sudo cloudflared tunnel run"
}, },
"devDependencies": { "devDependencies": {
"bun-types": "^0.1.2" "bun-types": "^0.1.2"

81
src/commands/github.ts Normal file
View file

@ -0,0 +1,81 @@
import { APIApplicationCommandInteractionDataStringOption, ApplicationCommandOptionType, InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
import { Command } from '../structures/Command';
// @ts-expect-error Types :(
import utilities from '../../files/utilities.toml';
// @ts-expect-error Types :(
import config from '../../files/config.toml';
import { githubIssuesAndPullRequests } from '../utils/regexes';
import isNumeric from '../utils/isNumeric';
import Collection from '@discordjs/collection';
const cooldowns: Collection<string, number> = new Collection();
new Command({
name: 'github',
description: 'Query an issue, pull request or direct link to Github Issue or PR',
options: [
{
name: 'query',
description: 'Issue, PR number or direct link to Github Issue or PR',
type: ApplicationCommandOptionType.String,
required: true
},
{
name: 'repository',
description: 'Project repository (default oven-sh/bun)',
type: ApplicationCommandOptionType.String,
required: false,
choices: [
...utilities.github.repositories.map(repository => new Object({
name: repository.split('/')[1],
value: repository
}))
]
}
],
run: async(ctx) => {
if (cooldowns.has(ctx.user.id) && cooldowns.get(ctx.user.id) < Date.now()) {
return ctx.respond('⚠️ You are in cooldown.');
}
const query: string = (ctx.options[0] as APIApplicationCommandInteractionDataStringOption).value;
const repository: string = (ctx.options?.[1] as APIApplicationCommandInteractionDataStringOption)?.value || 'oven-sh/bun';
const repositorySplit = repository.split('/');
const repositoryOwner = repositorySplit[0];
const repositoryName = repositorySplit[1];
const isIssueOrPR = githubIssuesAndPullRequests(repositoryOwner, repositoryName).test(query);
const isIssueOrPRNumber = isNumeric(query);
if (!isIssueOrPR && !isIssueOrPRNumber) {
return ctx.respond({
type: InteractionResponseType.ChannelMessageWithSource,
data: {
content: `\`\` Invalid issue or pull request \`${query}\``,
flags: MessageFlags.Ephemeral,
}
});
}
const issueUrl = `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/issues/${isIssueOrPR ? query.split('/issues/')[1] : query}`;
cooldowns.set(ctx.user.id, Date.now() + 60000);
const res = await fetch(issueUrl, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'bun-discord-bot',
'Authorization': config.api.github_personal_access_token
}
});
const data: any = await res.json();
// TODO: finish (pull request, issue)
return ctx.respond([
`[#${data.number} ${repositoryOwner}/${repositoryName}](${data.html_url}) operation <timestamp>`,
data.title
].join('\n'));
}
})

View file

@ -2,14 +2,13 @@ import { InteractionResponseType } from 'discord-api-types/v10';
import { Command } from '../structures/Command'; import { Command } from '../structures/Command';
new Command({ new Command({
name: 'help', name: 'ping',
description: 'Help command', description: 'pong',
guildId: '924395690451423332', run: (ctx) => {
run: (c) => { return ctx.respond({
return c.respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: 'hello' content: 'Pong 🏓'
} }
}) })
} }

View file

@ -5,7 +5,6 @@ import { findTags, getTag } from '../utils/tagsUtils';
new Command({ new Command({
name: 'tags', name: 'tags',
description: 'Send a tag by name or alias', description: 'Send a tag by name or alias',
guildId: '924395690451423332',
options: [ options: [
{ {
name: 'query', name: 'query',
@ -23,13 +22,13 @@ new Command({
required: false required: false
} }
], ],
run: (c) => { run: (ctx) => {
const query: APIApplicationCommandInteractionDataStringOption = c.options[0] as APIApplicationCommandInteractionDataStringOption; const query: APIApplicationCommandInteractionDataStringOption = ctx.options[0] as APIApplicationCommandInteractionDataStringOption;
const target = c?.resolved?.users?.[0]; const target = ctx?.resolved?.users?.[0];
const tag = getTag(query.value); const tag = getTag(query.value);
if (!tag) if (!tag)
return c.respond({ return ctx.respond({
type: InteractionResponseType.ChannelMessageWithSource, type: InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `\`\` Could not find a tag \`${query.value}\``, content: `\`\` Could not find a tag \`${query.value}\``,
@ -37,7 +36,7 @@ new Command({
} }
}); });
return c.respond([ return ctx.respond([
target ? `*Tag suggestion for <@${target.id}>:*` : '', target ? `*Tag suggestion for <@${target.id}>:*` : '',
tag.content tag.content
].join('\n')); ].join('\n'));

View file

@ -21,6 +21,7 @@ try {
} }
const app = new Hono(); const app = new Hono();
app.get('*', (c) => c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'));
app.post('/interaction', bodyParse(), async(c) => { app.post('/interaction', bodyParse(), async(c) => {
const signature = c.req.headers.get('X-Signature-Ed25519'); const signature = c.req.headers.get('X-Signature-Ed25519');
@ -62,6 +63,7 @@ app.post('/interaction', bodyParse(), async(c) => {
if (interaction.type === InteractionType.ApplicationCommand && interaction.data.type === ApplicationCommandType.ChatInput) { if (interaction.type === InteractionType.ApplicationCommand && interaction.data.type === ApplicationCommandType.ChatInput) {
return Commands.get(interaction.data.name).run(new CommandContext( return Commands.get(interaction.data.name).run(new CommandContext(
c, c,
interaction.user,
interaction.data.options, interaction.data.options,
interaction.data.resolved interaction.data.resolved
)); ));

View file

@ -1,5 +1,7 @@
// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Command.ts // Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Command.ts
// @ts-expect-error Types :(
import config from '../../files/config.toml';
import { LocaleString } from 'discord-api-types/v10'; import { LocaleString } from 'discord-api-types/v10';
import { Commands } from '../managers/CommandManager'; import { Commands } from '../managers/CommandManager';
import { CommandContext } from './contexts/CommandContext'; import { CommandContext } from './contexts/CommandContext';
@ -13,7 +15,7 @@ export interface CommandOptions {
guildId?: string; guildId?: string;
defaultMemberPermissions?: string; defaultMemberPermissions?: string;
options?: Option[] | OptionOptions[]; options?: Option[] | OptionOptions[];
run: (ctx: CommandContext) => Response; run: (ctx: CommandContext) => Response | Promise<Response>;
} }
export class Command { export class Command {
@ -21,10 +23,10 @@ export class Command {
public nameLocalizations?: Record<LocaleString, string>; public nameLocalizations?: Record<LocaleString, string>;
public description?: string; public description?: string;
public descriptionLocalizations?: Record<LocaleString, string>; public descriptionLocalizations?: Record<LocaleString, string>;
public guildId?: string; public guildId?: string = config.client.guild_id;
public defaultMemberPermissions?: string; public defaultMemberPermissions?: string;
public options: Option[] | OptionOptions[]; public options: Option[] | OptionOptions[];
public run: (ctx: CommandContext) => Response; public run: (ctx: CommandContext) => Response | Promise<Response>;
public constructor(options: CommandOptions) { public constructor(options: CommandOptions) {
this.name = options.name; this.name = options.name;

View file

@ -1,13 +1,15 @@
import { APIApplicationCommandInteractionDataOption, APIChatInputApplicationCommandInteractionDataResolved, APIInteractionResponse, InteractionResponseType } from 'discord-api-types/v10'; import { APIApplicationCommandInteractionDataOption, APIChatInputApplicationCommandInteractionDataResolved, APIInteractionResponse, APIUser, InteractionResponseType } from 'discord-api-types/v10';
import { Context } from 'hono'; import { Context } from 'hono';
export class CommandContext { export class CommandContext {
public context: Context; public context: Context;
public user?: APIUser;
public options?: APIApplicationCommandInteractionDataOption[]; public options?: APIApplicationCommandInteractionDataOption[];
public resolved?: APIChatInputApplicationCommandInteractionDataResolved; public resolved?: APIChatInputApplicationCommandInteractionDataResolved;
public constructor(c: Context, options?: APIApplicationCommandInteractionDataOption[], resolved?: APIChatInputApplicationCommandInteractionDataResolved) { public constructor(c: Context, user?: APIUser, options?: APIApplicationCommandInteractionDataOption[], resolved?: APIChatInputApplicationCommandInteractionDataResolved) {
this.context = c; this.context = c;
this.user = user;
this.options = options; this.options = options;
this.resolved = resolved; this.resolved = resolved;
} }

1
src/utils/isNumeric.ts Normal file
View file

@ -0,0 +1 @@
export default (value: string) => /^-?\d+$/.test(value);

2
src/utils/regexes.ts Normal file
View file

@ -0,0 +1,2 @@
export const githubIssuesAndPullRequests = (owner: string, repository: string) =>
new RegExp(`https?:\\\/\\\/github\\\.com\\\/${owner}\\\/${repository}\\\/(?:issues\\\/\\\d+|pull\\\/\d+)`, 'gm');