mirror of
https://github.com/xHyroM/bun-discord-bot.git
synced 2024-11-22 14:41:05 +01:00
refactor: rewrite
start working on full rewrite [part 1]
This commit is contained in:
parent
79a90da9a6
commit
a5f1742918
40 changed files with 252 additions and 1511 deletions
47
.github/workflows/validate.yml
vendored
47
.github/workflows/validate.yml
vendored
|
@ -1,34 +1,33 @@
|
||||||
name: Validate
|
name: Validate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: refs/pull/${{ github.event.number }}/merge
|
ref: refs/pull/${{ github.event.number }}/merge
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: xhyrom/setup-bun@v0.1.3
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event
|
uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Add json
|
- name: Update files.json file
|
||||||
run: cp $HOME/files.json ./scripts/validateTags/
|
run: cp $HOME/files.json ./scripts/validate_tags/
|
||||||
|
|
||||||
- name: Validate tag
|
- name: Validate tags
|
||||||
run: bun run validate
|
run: bun run validate:tags
|
||||||
env:
|
env:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-sha: ${{ github.event.pull_request.head.sha }}
|
commit-sha: ${{ github.event.pull_request.head.sha }}
|
||||||
pr-number: ${{ github.event.pull_request.number }}
|
pr-number: ${{ github.event.pull_request.number }}
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,8 +2,6 @@
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||||
|
|
||||||
files/database.sqlite
|
files/database.sqlite
|
||||||
files/config.toml
|
|
||||||
requests.rest
|
|
||||||
|
|
||||||
### Node ###
|
### Node ###
|
||||||
# Logs
|
# Logs
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"tabWidth": 4,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -1,16 +0,0 @@
|
||||||
# For start application rename it to config.toml !!
|
|
||||||
|
|
||||||
[server]
|
|
||||||
port = 3000
|
|
||||||
|
|
||||||
[client]
|
|
||||||
public_key = "CLIENT PUBLIC KEY"
|
|
||||||
token = "CLIENT TOKEN"
|
|
||||||
id = "CLIENT USER ID"
|
|
||||||
|
|
||||||
[commands]
|
|
||||||
guild_id = "GUILD ID"
|
|
||||||
|
|
||||||
[api]
|
|
||||||
github_personal_access_token = ""
|
|
||||||
github_webhooks_secret = ""
|
|
|
@ -1,4 +1,3 @@
|
||||||
[github]
|
|
||||||
repositories = [
|
repositories = [
|
||||||
"oven-sh/bun",
|
"oven-sh/bun",
|
||||||
"xHyroM/bun-discord-bot"
|
"xHyroM/bun-discord-bot"
|
14
package.json
14
package.json
|
@ -3,19 +3,13 @@
|
||||||
"name": "bun-discord-bot",
|
"name": "bun-discord-bot",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun src/index.ts",
|
"start": "bun src/index.ts",
|
||||||
"validate": "cd scripts/validateTags && bun install && bun start",
|
"validate:tags": "cd scripts/validate_tags && bun install && bun start"
|
||||||
"cloudflare:tunnel": "sudo cloudflared tunnel run"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "^0.1.8"
|
"bun-types": "^0.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^0.7.0",
|
"@paperdave/logger": "^3.0.1",
|
||||||
"bun-utilities": "^0.2.1",
|
"discord.js": "^14.11.0"
|
||||||
"create-hmac": "^1.1.7",
|
|
||||||
"discord-api-types": "^0.36.1",
|
|
||||||
"hono": "^1.6.4",
|
|
||||||
"minisearch": "^5.0.0",
|
|
||||||
"tweetnacl": "^1.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
[
|
|
||||||
"files/tags.toml"
|
|
||||||
]
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "validate-tags",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bun src/index.ts"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
interface Tag {
|
|
||||||
keywords: string[];
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubToken = process.env['github-token'];
|
|
||||||
const commitSha = process.env['commit-sha'];
|
|
||||||
const pullRequestNumber = process.env['pr-number'];
|
|
||||||
|
|
||||||
const codeBlockRegex = /(`{1,3}).+?\1/gs;
|
|
||||||
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/gi;
|
|
||||||
const files = await Bun.file('./files.json').text();
|
|
||||||
if (!files.includes('files/tags.toml')) process.exit(0);
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
let tags;
|
|
||||||
try {
|
|
||||||
// @ts-expect-error types
|
|
||||||
tags = (await import('../../../files/tags.toml')).default;
|
|
||||||
|
|
||||||
requestGithub(
|
|
||||||
`issues/${pullRequestNumber}/labels`,
|
|
||||||
{
|
|
||||||
labels: ['tags']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch(e) {
|
|
||||||
tags = [];
|
|
||||||
errors.push(e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(tags)) {
|
|
||||||
const tag = value as Tag;
|
|
||||||
|
|
||||||
if (!tag?.keywords || tag.keywords.length === 0) errors.push(`**[${key}]:** Tag must have keywords`);
|
|
||||||
if (tag?.keywords?.[0] !== key) errors.push(`**[${key}]:** First keyword of tag is not the same as the tag name`);
|
|
||||||
if (!tag.content) errors.push(`**[${key}]:** Tag must have content`);
|
|
||||||
|
|
||||||
if (tag.content) {
|
|
||||||
const cleanedContent = tag.content.replaceAll('+++', '```').replace(codeBlockRegex, '');
|
|
||||||
for (const url of cleanedContent.match(urlRegex) || []) {
|
|
||||||
const firstChar = tag.content.split(url)[0].slice(-1);
|
|
||||||
const lastChar = tag.content.split(url)[1].slice(0, 1);
|
|
||||||
if (
|
|
||||||
firstChar !== '<' ||
|
|
||||||
lastChar !== '>'
|
|
||||||
) errors.push(`**[${key}]:** Link must be wrapped in <>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag.keywords) {
|
|
||||||
const keywords = [...new Set(tag.keywords)];
|
|
||||||
if (keywords.length !== tag.keywords.length) errors.push(`**[${key}]:** Keywords must be unique`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length === 0) {
|
|
||||||
requestGithub(
|
|
||||||
`pulls/${pullRequestNumber}/reviews`,
|
|
||||||
{
|
|
||||||
commit_id: commitSha,
|
|
||||||
event: 'APPROVE',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
requestGithub(
|
|
||||||
`pulls/${pullRequestNumber}/requested_reviewers`,
|
|
||||||
{
|
|
||||||
reviewers: ['xHyroM']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
requestGithub(
|
|
||||||
`issues/${pullRequestNumber}/labels/waiting`,
|
|
||||||
{},
|
|
||||||
'DELETE'
|
|
||||||
);
|
|
||||||
|
|
||||||
requestGithub(
|
|
||||||
`issues/${pullRequestNumber}/labels`,
|
|
||||||
{
|
|
||||||
labels: ['ready']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
requestGithub(
|
|
||||||
`pulls/${pullRequestNumber}/reviews`,
|
|
||||||
{
|
|
||||||
commit_id: commitSha,
|
|
||||||
body: '### Please fix the following problems:\n' + errors.join('\n'),
|
|
||||||
event: 'REQUEST_CHANGES',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
requestGithub(
|
|
||||||
`issues/${pullRequestNumber}/labels`,
|
|
||||||
{
|
|
||||||
labels: ['waiting']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestGithub(url: string, body: any, method?: 'POST' | 'DELETE') {
|
|
||||||
fetch(`https://api.github.com/repos/xHyroM/bun-discord-bot/${url}`, {
|
|
||||||
method: method || 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/vnd.github+json',
|
|
||||||
'Authorization': `token ${githubToken}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export { };
|
|
1
scripts/validate_tags/files.json
Normal file
1
scripts/validate_tags/files.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
["files/tags.toml"]
|
6
scripts/validate_tags/package.json
Normal file
6
scripts/validate_tags/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "validate_tags",
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun src/index.ts"
|
||||||
|
}
|
||||||
|
}
|
108
scripts/validate_tags/src/index.ts
Normal file
108
scripts/validate_tags/src/index.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
interface Tag {
|
||||||
|
keywords: string[];
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubToken = process.env['github-token'];
|
||||||
|
const commitSha = process.env['commit-sha'];
|
||||||
|
const pullRequestNumber = process.env['pr-number'];
|
||||||
|
|
||||||
|
const codeBlockRegex = /(`{1,3}).+?\1/gs;
|
||||||
|
const urlRegex =
|
||||||
|
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/gi;
|
||||||
|
|
||||||
|
// Check if files/tags.toml was changed
|
||||||
|
const files = await Bun.file('./files.json').text();
|
||||||
|
if (!files.includes('files/tags.toml')) process.exit(0);
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
let tags;
|
||||||
|
try {
|
||||||
|
tags = (await import('../../../files/tags.toml')).default;
|
||||||
|
|
||||||
|
await requestGithub(`issues/${pullRequestNumber}/labels`, {
|
||||||
|
labels: ['tags'],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
tags = [];
|
||||||
|
errors.push(e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(tags)) {
|
||||||
|
const tag = value as Tag;
|
||||||
|
|
||||||
|
if (!tag?.keywords || tag.keywords.length === 0)
|
||||||
|
errors.push(`**[${key}]:** Tag must have keywords`);
|
||||||
|
if (tag?.keywords?.[0] !== key)
|
||||||
|
errors.push(
|
||||||
|
`**[${key}]:** First keyword of tag is not the same as the tag name`
|
||||||
|
);
|
||||||
|
if (!tag.content) errors.push(`**[${key}]:** Tag must have content`);
|
||||||
|
|
||||||
|
if (tag.content) {
|
||||||
|
const cleanedContent = tag.content
|
||||||
|
.replaceAll('+++', '```')
|
||||||
|
.replace(codeBlockRegex, '');
|
||||||
|
for (const url of cleanedContent.match(urlRegex) || []) {
|
||||||
|
const firstChar = tag.content.split(url)[0].slice(-1);
|
||||||
|
const lastChar = tag.content.split(url)[1].slice(0, 1);
|
||||||
|
if (firstChar !== '<' || lastChar !== '>')
|
||||||
|
errors.push(`**[${key}]:** Link must be wrapped in <>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.keywords) {
|
||||||
|
const keywords = [...new Set(tag.keywords)];
|
||||||
|
if (keywords.length !== tag.keywords.length)
|
||||||
|
errors.push(`**[${key}]:** Keywords must be unique`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
await requestGithub(`pulls/${pullRequestNumber}/reviews`, {
|
||||||
|
commit_id: commitSha,
|
||||||
|
event: 'APPROVE',
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestGithub(`pulls/${pullRequestNumber}/requested_reviewers`, {
|
||||||
|
reviewers: ['xHyroM'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestGithub(
|
||||||
|
`issues/${pullRequestNumber}/labels/waiting`,
|
||||||
|
{},
|
||||||
|
'DELETE'
|
||||||
|
);
|
||||||
|
|
||||||
|
await requestGithub(`issues/${pullRequestNumber}/labels`, {
|
||||||
|
labels: ['ready'],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await requestGithub(`pulls/${pullRequestNumber}/reviews`, {
|
||||||
|
commit_id: commitSha,
|
||||||
|
body: '### Please fix the following problems:\n' + errors.join('\n'),
|
||||||
|
event: 'REQUEST_CHANGES',
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestGithub(`issues/${pullRequestNumber}/labels`, {
|
||||||
|
labels: ['waiting'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestGithub(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
method?: 'POST' | 'DELETE'
|
||||||
|
) {
|
||||||
|
await fetch(`https://api.github.com/repos/xHyroM/bun-discord-bot/${url}`, {
|
||||||
|
method: method || 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.github+json',
|
||||||
|
Authorization: `token ${githubToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
4
scripts/validate_tags/src/types.d.ts
vendored
Normal file
4
scripts/validate_tags/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.toml' {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
39
src/Bient.ts
Normal file
39
src/Bient.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { info } from "@paperdave/logger";
|
||||||
|
import { Client, ClientOptions } from "discord.js";
|
||||||
|
import { redactToken } from "./utils";
|
||||||
|
|
||||||
|
export class Bient extends Client {
|
||||||
|
public static instance: Bient;
|
||||||
|
|
||||||
|
constructor(options: ClientOptions) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
Bient.instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async load(): Promise<void> {
|
||||||
|
info("Loading listeners and commands...");
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
await this.loadListeners();
|
||||||
|
await this.loadCommands();
|
||||||
|
|
||||||
|
const end = performance.now();
|
||||||
|
|
||||||
|
info(`Loaded listeners and commands in ${end - start}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadCommands(): Promise<void> {
|
||||||
|
await import("./commands/Ping.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadListeners(): Promise<void> {
|
||||||
|
await import("./listeners/Ready.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public login(): Promise<string> {
|
||||||
|
info(`Logging in using ${redactToken(process.env.BOT_TOKEN)}`);
|
||||||
|
return super.login(process.env.BOT_TOKEN);
|
||||||
|
}
|
||||||
|
}
|
9
src/commands/Ping.ts
Normal file
9
src/commands/Ping.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { CommandInteraction } from "discord.js";
|
||||||
|
import { Command } from "../decorators/Command";
|
||||||
|
|
||||||
|
@Command("ping")
|
||||||
|
export class Ping {
|
||||||
|
async run(context: CommandInteraction) {
|
||||||
|
context.reply("Pong!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,135 +0,0 @@
|
||||||
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';
|
|
||||||
import { CommandContext } from '../structures/contexts/CommandContext';
|
|
||||||
import { getIssueOrPR, search, formatStatus, formatEmojiStatus, IssueState, IssueType } from '../utils/githubUtils';
|
|
||||||
|
|
||||||
const invalidIssue = (ctx: CommandContext, query: string) => {
|
|
||||||
return ctx.editResponse(
|
|
||||||
`\`❌\` Invalid issue or pull request \`${query}\`. You can check [github search syntax](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
new Command({
|
|
||||||
name: 'github',
|
|
||||||
description: 'Query an issue, pull request or direct link to Github Issue or PR',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'query',
|
|
||||||
description: 'Issue numer/name, PR number/name or direct link to Github Issue or PR',
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
required: true,
|
|
||||||
run: async(ctx) => {
|
|
||||||
return ctx.respond(
|
|
||||||
await search(
|
|
||||||
ctx.value,
|
|
||||||
(ctx.options.find(o => o.name === 'repository'))?.value as string || 'oven-sh/bun',
|
|
||||||
(ctx.options.find(o => o.name === 'state')?.value as string || 'all') as IssueState,
|
|
||||||
(ctx.options.find(o => o.name === 'type')?.value as string || '(IS|PR)') as IssueType,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'state',
|
|
||||||
description: 'Issue or PR state',
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
required: false,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: 'open',
|
|
||||||
value: 'open',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'closed',
|
|
||||||
value: 'closed',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'merged',
|
|
||||||
value: 'merged',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'all (default)',
|
|
||||||
value: 'all',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
description: 'Issues or PRs',
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
required: false,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: 'Issues',
|
|
||||||
value: '(IS)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Pull Requests',
|
|
||||||
value: '(PR)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Both',
|
|
||||||
value: '(IS|PR)',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => {
|
|
||||||
ctx.command.runEditResponse(ctx)
|
|
||||||
return ctx.respond({
|
|
||||||
type: InteractionResponseType.DeferredChannelMessageWithSource
|
|
||||||
})
|
|
||||||
},
|
|
||||||
runEditResponse: async(ctx) => {
|
|
||||||
let query: string = (ctx.options[0] as APIApplicationCommandInteractionDataStringOption).value;
|
|
||||||
const repository: string = (ctx.options.find(o => o.name === 'repository') as APIApplicationCommandInteractionDataStringOption)?.value || 'oven-sh/bun';
|
|
||||||
const state: IssueState = ((ctx.options.find(o => o.name === 'state') as APIApplicationCommandInteractionDataStringOption)?.value || 'all') as IssueState;
|
|
||||||
const type: IssueType = ((ctx.options.find(o => o.name === 'type') as APIApplicationCommandInteractionDataStringOption)?.value || '(IS|PR)') as IssueType;
|
|
||||||
|
|
||||||
const repositorySplit = repository.split('/');
|
|
||||||
const repositoryOwner = repositorySplit[0];
|
|
||||||
const repositoryName = repositorySplit[1];
|
|
||||||
|
|
||||||
let issueOrPR = await getIssueOrPR(parseInt(query), repository, state, type);
|
|
||||||
if (!issueOrPR) {
|
|
||||||
const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(query)}${encodeURIComponent(' repo:oven-sh/bun')}`);
|
|
||||||
|
|
||||||
const data: any = await res.json();
|
|
||||||
if (data.message || data?.items?.length === 0) return invalidIssue(ctx, query);
|
|
||||||
|
|
||||||
const item = data.items[0];
|
|
||||||
issueOrPR = {
|
|
||||||
id: item.number,
|
|
||||||
repository: item.repository_url.replace('https://api.github.com/repos/', ''),
|
|
||||||
title: item.title,
|
|
||||||
number: item.number,
|
|
||||||
state: item.state,
|
|
||||||
created_at: item.created_at,
|
|
||||||
closed_at: item.closed_at,
|
|
||||||
html_url: item.html_url,
|
|
||||||
user_login: item.user.login,
|
|
||||||
user_html_url: item.user.html_url,
|
|
||||||
type: item.pull_request ? '(PR)' : '(IS)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.editResponse([
|
|
||||||
`${formatEmojiStatus(issueOrPR)} [#${issueOrPR.number} ${issueOrPR.title.replace(/\[\]/g, '').slice(0, 1500)} (${repositoryOwner}/${repositoryName})](<${issueOrPR.html_url}>) by [${issueOrPR.user_login}](<${issueOrPR.user_html_url}>) ${formatStatus(issueOrPR)}`,
|
|
||||||
issueOrPR.title
|
|
||||||
].join('\n'));
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
|
|
||||||
new Command({
|
|
||||||
name: 'ping',
|
|
||||||
description: 'pong',
|
|
||||||
run: (ctx) => {
|
|
||||||
return ctx.respond({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: 'Pong 🏓',
|
|
||||||
flags: MessageFlags.Ephemeral,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,54 +0,0 @@
|
||||||
import {
|
|
||||||
APIApplicationCommandInteractionDataStringOption,
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
InteractionResponseType,
|
|
||||||
MessageFlags,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
import { findTags, getTag } from '../utils/tagsUtils';
|
|
||||||
|
|
||||||
new Command({
|
|
||||||
name: 'tag',
|
|
||||||
description: 'Send a tag by name or alias',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'query',
|
|
||||||
description: 'Tag name or alias',
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
required: true,
|
|
||||||
run: (ctx) => {
|
|
||||||
return ctx.respond(findTags(ctx.value));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'target',
|
|
||||||
description: 'User to mention',
|
|
||||||
type: ApplicationCommandOptionType.User,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
run: (ctx) => {
|
|
||||||
const query: APIApplicationCommandInteractionDataStringOption = ctx
|
|
||||||
.options[0] as APIApplicationCommandInteractionDataStringOption;
|
|
||||||
const target = ctx?.resolved?.users
|
|
||||||
? Object.values(ctx?.resolved?.users)[0]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const tag = getTag(query.value, false);
|
|
||||||
if (!tag)
|
|
||||||
return ctx.respond({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: `\`❌\` Could not find a tag \`${query.value}\``,
|
|
||||||
flags: MessageFlags.Ephemeral,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx.respond(
|
|
||||||
[
|
|
||||||
target ? `*Tag suggestion for <@${target.id}>:*` : '',
|
|
||||||
tag.content,
|
|
||||||
].join('\n')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
import { exec } from 'bun-utilities/spawn';
|
|
||||||
|
|
||||||
const commitHash = exec(['git', 'log', '--pretty=format:\'%h\'', '-n', '1']).stdout.replaceAll('\'', '');
|
|
||||||
|
|
||||||
new Command({
|
|
||||||
name: 'version',
|
|
||||||
description: 'Check bot and bun version',
|
|
||||||
run: (ctx) => {
|
|
||||||
return ctx.respond({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: [
|
|
||||||
`Bot version: [git-bun-discord-bot-"${commitHash}"](<https://github.com/xHyroM/bun-discord-bot/commit/${commitHash}>)`,
|
|
||||||
`Bun version: [${process.version}](<https://github.com/oven-sh/bun/releases/tag/bun-${process.version}>)`,
|
|
||||||
].join('\n'),
|
|
||||||
flags: MessageFlags.Ephemeral,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
6
src/decorators/Command.ts
Normal file
6
src/decorators/Command.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { ClientEvents } from "discord.js";
|
||||||
|
import { Bient } from "../Bient";
|
||||||
|
|
||||||
|
export function Command(name: string) {
|
||||||
|
return (constructor: Function, context: ClassDecoratorContext) => {};
|
||||||
|
}
|
12
src/decorators/Listener.ts
Normal file
12
src/decorators/Listener.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ClientEvents } from "discord.js";
|
||||||
|
import { Bient } from "../Bient";
|
||||||
|
|
||||||
|
export function Listener(event: keyof ClientEvents) {
|
||||||
|
return (constructor: Function, context: ClassMethodDecoratorContext) => {
|
||||||
|
const name = context as unknown as string;
|
||||||
|
|
||||||
|
Bient.instance.on.bind(Bient.instance)(event, (...args) =>
|
||||||
|
constructor[name]([...args])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
224
src/index.ts
224
src/index.ts
|
@ -1,222 +1,10 @@
|
||||||
import { Hono } from 'hono';
|
import { GatewayIntentBits } from "discord.js";
|
||||||
import { bodyParse } from 'hono/body-parse';
|
import { Bient } from "./Bient.ts";
|
||||||
import { Logger } from './utils/Logger';
|
|
||||||
|
|
||||||
// @ts-expect-error Types :(
|
const client = new Bient({
|
||||||
import config from '../files/config.toml';
|
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
|
||||||
import loadCommands from './utils/loadCommands';
|
|
||||||
import { verifyGithubKey, verifyKey } from './utils/verify';
|
|
||||||
import {
|
|
||||||
APIPingInteraction,
|
|
||||||
APIApplicationCommandInteraction,
|
|
||||||
APIMessageComponentInteraction,
|
|
||||||
InteractionType,
|
|
||||||
InteractionResponseType,
|
|
||||||
ApplicationCommandType,
|
|
||||||
APIApplicationCommandAutocompleteInteraction,
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import { CommandContext } from './structures/contexts/CommandContext';
|
|
||||||
import { Commands } from './managers/CommandManager';
|
|
||||||
import registerCommands from './utils/registerCommands';
|
|
||||||
import { Option, OptionOptions } from './structures/Option';
|
|
||||||
import { AutocompleteContext } from './structures/contexts/AutocompleteContext';
|
|
||||||
import {
|
|
||||||
deleteIssueOrPR,
|
|
||||||
fetchIssues,
|
|
||||||
fetchPullRequests,
|
|
||||||
setIssue,
|
|
||||||
setPullRequest,
|
|
||||||
} from './utils/githubUtils';
|
|
||||||
import { removeExclamationFromNicknames } from './utils/discord';
|
|
||||||
|
|
||||||
await fetchIssues();
|
|
||||||
await fetchPullRequests();
|
|
||||||
(async () => {
|
|
||||||
Logger.info('Removing exclamation marks from nicknames...');
|
|
||||||
await removeExclamationFromNicknames(config.client.token);
|
|
||||||
Logger.info('Removing is done!');
|
|
||||||
})();
|
|
||||||
await loadCommands();
|
|
||||||
try {
|
|
||||||
await registerCommands(config.client.token, config.client.id);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
app.get('*', (c) => c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME')); // fireship :D
|
|
||||||
|
|
||||||
app.post('/interaction', bodyParse(), async (c) => {
|
|
||||||
const signature = c.req.headers.get('X-Signature-Ed25519');
|
|
||||||
const timestamp = c.req.headers.get('X-Signature-Timestamp');
|
|
||||||
if (!signature || !timestamp)
|
|
||||||
return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
|
||||||
if (
|
|
||||||
!(await verifyKey(
|
|
||||||
JSON.stringify(c.req.parsedBody),
|
|
||||||
signature,
|
|
||||||
timestamp,
|
|
||||||
config.client.public_key
|
|
||||||
))
|
|
||||||
)
|
|
||||||
return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
|
||||||
|
|
||||||
const interaction = c.req.parsedBody as unknown as
|
|
||||||
| APIPingInteraction
|
|
||||||
| APIApplicationCommandInteraction
|
|
||||||
| APIMessageComponentInteraction
|
|
||||||
| APIApplicationCommandAutocompleteInteraction;
|
|
||||||
|
|
||||||
if (interaction.type === InteractionType.Ping) {
|
|
||||||
return new CommandContext(c).respond({
|
|
||||||
type: InteractionResponseType.Pong,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
interaction.type === InteractionType.ApplicationCommandAutocomplete &&
|
|
||||||
interaction.data.type === ApplicationCommandType.ChatInput
|
|
||||||
) {
|
|
||||||
const command = Commands.get(interaction.data.name);
|
|
||||||
let options = command.options;
|
|
||||||
const subCommandGroup = interaction.data.options.find(
|
|
||||||
(option) =>
|
|
||||||
option.type === ApplicationCommandOptionType.SubcommandGroup
|
|
||||||
);
|
|
||||||
const subCommand = interaction.data.options.find(
|
|
||||||
(option) => option.type === ApplicationCommandOptionType.Subcommand
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-expect-error ?? find
|
|
||||||
if (subCommandGroup)
|
|
||||||
options = options.find(
|
|
||||||
(option) => option.name === subCommandGroup.name
|
|
||||||
)?.options;
|
|
||||||
// @ts-expect-error ?? find
|
|
||||||
if (subCommand)
|
|
||||||
options = options.find(
|
|
||||||
(option) => option.name === subCommand.name
|
|
||||||
)?.options;
|
|
||||||
|
|
||||||
// @ts-expect-error i dont want waste time
|
|
||||||
const focused: APIApplicationCommandBasicOption =
|
|
||||||
interaction.data.options.find((option) => option.focused === true);
|
|
||||||
// @ts-expect-error ?? find
|
|
||||||
const option: Option | OptionOptions = options.find(
|
|
||||||
(option) => option.name === focused.name
|
|
||||||
);
|
|
||||||
|
|
||||||
return option.run(
|
|
||||||
new AutocompleteContext(
|
|
||||||
c,
|
|
||||||
option,
|
|
||||||
focused.value,
|
|
||||||
interaction.data.options as any
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
interaction.type === InteractionType.ApplicationCommand &&
|
|
||||||
interaction.data.type === ApplicationCommandType.ChatInput
|
|
||||||
) {
|
|
||||||
const commands = Commands.get(interaction.data.name);
|
|
||||||
return await commands.run(new CommandContext(c, commands, interaction));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandContext(c).respond({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: 'Beep boop. Boop beep?',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/github_webhook', bodyParse(), (c) => {
|
await client.load();
|
||||||
if (
|
|
||||||
!c.req.headers.get('User-Agent').startsWith('GitHub-Hookshot/') ||
|
|
||||||
typeof c.req?.parsedBody !== 'object'
|
|
||||||
)
|
|
||||||
return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
|
||||||
|
|
||||||
if (
|
client.login();
|
||||||
!verifyGithubKey(
|
|
||||||
JSON.stringify(c.req.parsedBody),
|
|
||||||
c.req.headers.get('X-Hub-Signature-256'),
|
|
||||||
config.api.github_webhooks_secret
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
|
||||||
|
|
||||||
const issueOrPr = c.req.parsedBody;
|
|
||||||
if (issueOrPr.action !== 'deleted') {
|
|
||||||
if ('issue' in issueOrPr) {
|
|
||||||
setIssue({
|
|
||||||
id: issueOrPr.issue.number,
|
|
||||||
repository: issueOrPr.issue.repository_url.replace(
|
|
||||||
'https://api.github.com/repos/',
|
|
||||||
''
|
|
||||||
),
|
|
||||||
title: issueOrPr.issue.title,
|
|
||||||
number: issueOrPr.issue.number,
|
|
||||||
state: issueOrPr.issue.state,
|
|
||||||
created_at: issueOrPr.issue.created_at,
|
|
||||||
closed_at: issueOrPr.issue.closed_at,
|
|
||||||
html_url: issueOrPr.issue.html_url,
|
|
||||||
user_login: issueOrPr.issue.user.login,
|
|
||||||
user_html_url: issueOrPr.issue.user.html_url,
|
|
||||||
type: '(IS)',
|
|
||||||
});
|
|
||||||
} else if ('pull_request' in issueOrPr) {
|
|
||||||
setPullRequest({
|
|
||||||
id: issueOrPr.pull_request.number,
|
|
||||||
repository: issueOrPr.pull_request.html_url
|
|
||||||
.replace('https://github.com/', '')
|
|
||||||
.replace(`/pull/${issueOrPr.pull_request.number}`, ''),
|
|
||||||
title: issueOrPr.pull_request.title,
|
|
||||||
number: issueOrPr.pull_request.number,
|
|
||||||
state: issueOrPr.pull_request.state,
|
|
||||||
created_at: issueOrPr.pull_request.created_at,
|
|
||||||
closed_at: issueOrPr.pull_request.closed_at,
|
|
||||||
merged_at: issueOrPr.pull_request.merged_at,
|
|
||||||
html_url: issueOrPr.pull_request.html_url,
|
|
||||||
user_login: issueOrPr.pull_request.user.login,
|
|
||||||
user_html_url: issueOrPr.pull_request.user.html_url,
|
|
||||||
type: '(PR)',
|
|
||||||
draft: issueOrPr.pull_request.draft,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ('issue' in issueOrPr)
|
|
||||||
deleteIssueOrPR(
|
|
||||||
issueOrPr.issue.number,
|
|
||||||
issueOrPr.issue.repository_url.replace(
|
|
||||||
'https://api.github.com/repos/',
|
|
||||||
''
|
|
||||||
)
|
|
||||||
);
|
|
||||||
else if ('pull_request' in issueOrPr)
|
|
||||||
deleteIssueOrPR(
|
|
||||||
issueOrPr.pull_request.number,
|
|
||||||
issueOrPr.pull_request.html_url
|
|
||||||
.replace('https://github.com/', '')
|
|
||||||
.replace(`/pull/${issueOrPr.pull_request.number}`, '')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
message: 'OK',
|
|
||||||
},
|
|
||||||
200
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Bun.serve({
|
|
||||||
port: config.server.port,
|
|
||||||
fetch: app.fetch,
|
|
||||||
});
|
|
||||||
|
|
||||||
Logger.info('🚀 Server started at', config.server.port.toString());
|
|
||||||
Logger.debug(`🌍 http://localhost:${config.server.port}`);
|
|
||||||
|
|
10
src/listeners/Ready.ts
Normal file
10
src/listeners/Ready.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { success } from "@paperdave/logger";
|
||||||
|
import { Listener } from "../decorators/Listener";
|
||||||
|
import { ArgsOf } from "../utils";
|
||||||
|
|
||||||
|
export class Ready {
|
||||||
|
@Listener("ready")
|
||||||
|
onReady([client]: ArgsOf<"ready">) {
|
||||||
|
success(`Logged in as ${client.user?.tag}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
import Collection from '@discordjs/collection';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
|
|
||||||
class CommandManager extends Collection<String, Command> {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public register(command: Command): CommandManager {
|
|
||||||
this.set(command.name, command);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unregister(command: Command): CommandManager {
|
|
||||||
this.delete(command.name);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Commands = new CommandManager();
|
|
|
@ -1,64 +0,0 @@
|
||||||
// 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 { Commands } from '../managers/CommandManager';
|
|
||||||
import { CommandContext } from './contexts/CommandContext';
|
|
||||||
import { Option, OptionOptions } from './Option';
|
|
||||||
|
|
||||||
export interface CommandOptions {
|
|
||||||
name: string;
|
|
||||||
nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
description?: string;
|
|
||||||
descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
guildId?: string;
|
|
||||||
defaultMemberPermissions?: string;
|
|
||||||
options?: Option[] | OptionOptions[];
|
|
||||||
run: (ctx: CommandContext) => Response | Promise<Response>;
|
|
||||||
runEditResponse?: (ctx: CommandContext) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Command {
|
|
||||||
public name: string;
|
|
||||||
public nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
public description?: string;
|
|
||||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
public guildId?: string = config.client.guild_id;
|
|
||||||
public defaultMemberPermissions?: string;
|
|
||||||
public options: Option[] | OptionOptions[];
|
|
||||||
public run: (ctx: CommandContext) => Response | Promise<Response>;
|
|
||||||
public runEditResponse: (ctx: CommandContext) => any;
|
|
||||||
|
|
||||||
public constructor(options: CommandOptions) {
|
|
||||||
this.name = options.name;
|
|
||||||
this.nameLocalizations = options.nameLocalizations;
|
|
||||||
|
|
||||||
this.description = options.description;
|
|
||||||
this.descriptionLocalizations = options.descriptionLocalizations;
|
|
||||||
|
|
||||||
this.guildId = options.guildId;
|
|
||||||
this.defaultMemberPermissions = options.defaultMemberPermissions;
|
|
||||||
|
|
||||||
this.options = options.options?.map(option => {
|
|
||||||
if (option instanceof Option) return option;
|
|
||||||
else return new Option(option);
|
|
||||||
});
|
|
||||||
this.run = options.run;
|
|
||||||
this.runEditResponse = options.runEditResponse;
|
|
||||||
|
|
||||||
Commands.register(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): Record<string, any> {
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.nameLocalizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.descriptionLocalizations,
|
|
||||||
guild_id: this.guildId,
|
|
||||||
default_member_permissions: this.defaultMemberPermissions,
|
|
||||||
options: this.options?.map(option => option.toJSON()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Argument.ts
|
|
||||||
|
|
||||||
import { ApplicationCommandOptionType, ChannelType, LocaleString } from 'discord-api-types/v10';
|
|
||||||
import { AutocompleteContext } from './contexts/AutocompleteContext';
|
|
||||||
|
|
||||||
export interface OptionChoice {
|
|
||||||
name: string;
|
|
||||||
nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionOptions {
|
|
||||||
name: string;
|
|
||||||
nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
description: string;
|
|
||||||
descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
type: ApplicationCommandOptionType
|
|
||||||
required?: boolean;
|
|
||||||
choices?: OptionChoice[];
|
|
||||||
options?: Array<Option | OptionOptions>;
|
|
||||||
channelTypes?: ChannelType[];
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
minLength?: number;
|
|
||||||
maxLength?: number;
|
|
||||||
run?: (ctx: AutocompleteContext) => Response | Promise<Response>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Option {
|
|
||||||
public name: string;
|
|
||||||
public nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
public description: string;
|
|
||||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
public type: ApplicationCommandOptionType;
|
|
||||||
public required?: boolean;
|
|
||||||
public choices?: Array<OptionChoice>;
|
|
||||||
public options?: Array<Option>;
|
|
||||||
public channelTypes?: Array<ChannelType | keyof typeof ChannelType>;
|
|
||||||
public minValue?: number;
|
|
||||||
public maxValue?: number;
|
|
||||||
public minLength?: number;
|
|
||||||
public maxLength?: number;
|
|
||||||
public run?: (ctx: AutocompleteContext) => Response | Promise<Response>;
|
|
||||||
|
|
||||||
public constructor(options: OptionOptions) {
|
|
||||||
this.name = options.name;
|
|
||||||
this.nameLocalizations = options.nameLocalizations;
|
|
||||||
this.description = options.description;
|
|
||||||
this.descriptionLocalizations = options.descriptionLocalizations;
|
|
||||||
this.type = options.type;
|
|
||||||
this.required = options.required;
|
|
||||||
this.choices = options.choices;
|
|
||||||
this.options = options.options?.map(argument => {
|
|
||||||
if (argument instanceof Option) return argument;
|
|
||||||
else return new Option(argument);
|
|
||||||
});
|
|
||||||
this.channelTypes = options.channelTypes;
|
|
||||||
this.minValue = options.minValue;
|
|
||||||
this.maxValue = options.maxValue;
|
|
||||||
this.minLength = options.minLength;
|
|
||||||
this.maxLength = options.maxLength;
|
|
||||||
this.run = options.run;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): Record<string, any> {
|
|
||||||
if (
|
|
||||||
this.type === ApplicationCommandOptionType.Subcommand ||
|
|
||||||
this.type === ApplicationCommandOptionType.SubcommandGroup
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.nameLocalizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.descriptionLocalizations,
|
|
||||||
type: this.type,
|
|
||||||
options: this.options?.map(option => option.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.nameLocalizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.descriptionLocalizations,
|
|
||||||
type: this.type,
|
|
||||||
required: this.required,
|
|
||||||
choices: this.choices,
|
|
||||||
channel_types: this.channelTypes,
|
|
||||||
min_value: this.minValue,
|
|
||||||
max_value: this.maxValue,
|
|
||||||
min_length: this.minLength,
|
|
||||||
max_length: this.maxLength,
|
|
||||||
autocomplete: typeof this.run === 'function',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { APIApplicationCommandInteractionDataBasicOption, APIApplicationCommandOptionChoice, InteractionResponseType } from 'discord-api-types/v10';
|
|
||||||
import { Context } from 'hono';
|
|
||||||
import { Option, OptionOptions } from '../Option';
|
|
||||||
|
|
||||||
export class AutocompleteContext {
|
|
||||||
public context: Context;
|
|
||||||
public option?: Option | OptionOptions;
|
|
||||||
public value?: string;
|
|
||||||
public options?: APIApplicationCommandInteractionDataBasicOption[];
|
|
||||||
|
|
||||||
public constructor(c: Context, option: Option | OptionOptions, value: string, options: APIApplicationCommandInteractionDataBasicOption[]) {
|
|
||||||
this.context = c;
|
|
||||||
this.option = option;
|
|
||||||
this.value = value;
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public respond(response: APIApplicationCommandOptionChoice[]) {
|
|
||||||
return this.context.json({
|
|
||||||
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { APIApplicationCommandInteraction, APIApplicationCommandInteractionDataOption, APIChatInputApplicationCommandInteraction, APIChatInputApplicationCommandInteractionDataResolved, APIInteractionGuildMember, APIInteractionResponse, APIInteractionResponseCallbackData, APIUser, ApplicationCommandType, InteractionResponseType, InteractionType, RouteBases, Routes } from 'discord-api-types/v10';
|
|
||||||
import { Context } from 'hono';
|
|
||||||
import { Command } from '../Command';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import config from '../../../files/config.toml';
|
|
||||||
|
|
||||||
export class CommandContext {
|
|
||||||
public context: Context;
|
|
||||||
public command?: Command;
|
|
||||||
public interaction?: APIChatInputApplicationCommandInteraction;
|
|
||||||
public user?: APIUser;
|
|
||||||
public member?: APIInteractionGuildMember;
|
|
||||||
public options?: APIApplicationCommandInteractionDataOption[];
|
|
||||||
public resolved?: APIChatInputApplicationCommandInteractionDataResolved;
|
|
||||||
|
|
||||||
public constructor(c: Context, command?: Command, interaction?: APIApplicationCommandInteraction) {
|
|
||||||
this.context = c;
|
|
||||||
this.command = command;
|
|
||||||
|
|
||||||
if (interaction) {
|
|
||||||
this.interaction = interaction as APIChatInputApplicationCommandInteraction;
|
|
||||||
this.user = this.interaction.member.user;
|
|
||||||
this.member = this.interaction.member;
|
|
||||||
this.options = this.interaction.data.options;
|
|
||||||
this.resolved = this.interaction.data.resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public respond(response: APIInteractionResponse | string) {
|
|
||||||
if (typeof response === 'string') {
|
|
||||||
return this.context.json({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.context.json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async editResponse(response: APIInteractionResponseCallbackData | string) {
|
|
||||||
if (typeof response === 'string') {
|
|
||||||
response = {
|
|
||||||
content: response
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${RouteBases.api}${Routes.webhookMessage(this.interaction.application_id, this.interaction.token)}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bot ${config.client.token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
13
src/utils.ts
Normal file
13
src/utils.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { ClientEvents } from "discord.js";
|
||||||
|
|
||||||
|
export type ArgsOf<K extends keyof ClientEvents> = ClientEvents[K];
|
||||||
|
|
||||||
|
export const redactToken = (token: string): string => {
|
||||||
|
return token
|
||||||
|
.split(".")
|
||||||
|
.map((part, index) => {
|
||||||
|
if (index === 0) return part;
|
||||||
|
return "*".repeat(part.length);
|
||||||
|
})
|
||||||
|
.join(".");
|
||||||
|
};
|
|
@ -1,47 +0,0 @@
|
||||||
type Level = 'INFO' | 'SUCCESS' | 'WARN' | 'ERROR' | 'DEBUG'
|
|
||||||
|
|
||||||
export class Logger extends null {
|
|
||||||
public static info(...messages: any[]) {
|
|
||||||
this.log('INFO', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static success(...messages: any[]) {
|
|
||||||
this.log('SUCCESS', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static warn(...messages: any[]) {
|
|
||||||
this.log('WARN', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static error(...messages: any[]) {
|
|
||||||
this.log('ERROR', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static debug(...messages: any[]) {
|
|
||||||
this.log('DEBUG', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static log(level: Level, messages: any[]) {
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
let color = '';
|
|
||||||
switch (level) {
|
|
||||||
case 'INFO':
|
|
||||||
color = '\x1b[36m';
|
|
||||||
break;
|
|
||||||
case 'SUCCESS':
|
|
||||||
color = '\u001b[32m';
|
|
||||||
break;
|
|
||||||
case 'WARN':
|
|
||||||
color = '\x1b[93m';
|
|
||||||
break;
|
|
||||||
case 'ERROR':
|
|
||||||
color = '\x1b[91m';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
color = '\x1b[2m';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${color}[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}/${level}]\x1b[0m`, ...messages);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { Logger } from "./Logger";
|
|
||||||
|
|
||||||
export const getDiscordGuildMembers = async(token: string) => {
|
|
||||||
let oldId;
|
|
||||||
const result: any[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const members: any[] = await (await fetch(
|
|
||||||
`https://discord.com/api/v10/guilds/876711213126520882/members?limit=1000${oldId ? `&after=${oldId}` : ''}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)).json();
|
|
||||||
|
|
||||||
if (members.length == 0) break;
|
|
||||||
|
|
||||||
console.log(members);
|
|
||||||
result.push(...members.map(m => ({ id: m.id, nickname: m.nick })));
|
|
||||||
oldId = members[members.length - 1].id;
|
|
||||||
|
|
||||||
Logger.debug(`Fetching guild members - ${result.length}, ${oldId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`All guild members has been fetched - ${result.length}`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeExclamationFromNicknames = async(token: string) => {
|
|
||||||
for (const member of await getDiscordGuildMembers(token)) {
|
|
||||||
if (!member.nickname?.startsWith?.('!')) continue;
|
|
||||||
|
|
||||||
await fetch(`https://discord.com/api/v8/guilds/876711213126520882/members/${member.id}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
nick: member.nickname.slice(1),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import config from '../../files/config.toml';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import utilities from '../../files/utilities.toml';
|
|
||||||
import MiniSearch from 'minisearch';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
import { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
|
||||||
import { Database } from 'bun:sqlite';
|
|
||||||
import { githubTitleClean } from './regexes';
|
|
||||||
|
|
||||||
export type IssueState = 'open' | 'closed' | 'all' | 'merged';
|
|
||||||
export type IssueType = '(IS)' | '(PR)' | '(IS|PR)';
|
|
||||||
interface Issue {
|
|
||||||
id: number;
|
|
||||||
repository: string;
|
|
||||||
title: string;
|
|
||||||
number: number;
|
|
||||||
state: IssueState,
|
|
||||||
created_at: string;
|
|
||||||
closed_at: string | null;
|
|
||||||
html_url: string;
|
|
||||||
user_login: string;
|
|
||||||
user_html_url: string;
|
|
||||||
type: IssueType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PullRequest extends Issue {
|
|
||||||
merged_at: string | null;
|
|
||||||
draft: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = new Database('./files/database.sqlite');
|
|
||||||
await db.exec('DROP TABLE IF EXISTS issuesandprs');
|
|
||||||
await db.exec('CREATE TABLE issuesandprs (id INTEGER PRIMARY KEY, repository TEXT, title TEXT, number INTEGER, state TEXT, created_at TEXT, closed_at TEXT, merged_at TEXT, html_url TEXT, user_login TEXT, user_html_url TEXT, type TEXT, draft TINYINT)');
|
|
||||||
|
|
||||||
const addToDb = db.prepare(
|
|
||||||
'INSERT INTO issuesandprs (repository, title, number, state, created_at, closed_at, merged_at, html_url, user_login, user_html_url, type, draft) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
||||||
);
|
|
||||||
|
|
||||||
export let issues: number = 0;
|
|
||||||
export let pulls: number = 0;
|
|
||||||
|
|
||||||
export const fetchIssues = async() => {
|
|
||||||
for await (const repository of utilities.github.repositories) {
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const res = await (await fetch(`https://api.github.com/repos/${repository}/issues?per_page=100&page=${page}&state=all`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'bun-discord-bot',
|
|
||||||
'Authorization': `token ${config.api.github_personal_access_token}`
|
|
||||||
}
|
|
||||||
})).json() as any;
|
|
||||||
|
|
||||||
for (const issue of res) {
|
|
||||||
if ('pull_request' in issue) continue;
|
|
||||||
|
|
||||||
// @ts-expect-error it works
|
|
||||||
await addToDb.run([
|
|
||||||
issue.repository_url.replace('https://api.github.com/repos/', ''),
|
|
||||||
issue.title,
|
|
||||||
issue.number,
|
|
||||||
issue.state,
|
|
||||||
issue.created_at,
|
|
||||||
issue.closed_at,
|
|
||||||
null,
|
|
||||||
issue.html_url,
|
|
||||||
issue.user.login,
|
|
||||||
issue.user.html_url,
|
|
||||||
'(IS)',
|
|
||||||
null,
|
|
||||||
]);
|
|
||||||
issues++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`Fetching issues for ${repository} - ${issues} * ${page}`);
|
|
||||||
|
|
||||||
page++;
|
|
||||||
if (res.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.success(`Issues have been fetched for ${repository} - ${issues}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
issues = null;
|
|
||||||
Object.freeze(issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchPullRequests = async() => {
|
|
||||||
for await (const repository of utilities.github.repositories) {
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const res = await (await fetch(`https://api.github.com/repos/${repository}/pulls?per_page=100&page=${page}&state=all`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'bun-discord-bot',
|
|
||||||
'Authorization': `token ${config.api.github_personal_access_token}`
|
|
||||||
}
|
|
||||||
})).json() as any;
|
|
||||||
|
|
||||||
for (const pull of res) {
|
|
||||||
// @ts-expect-error it works
|
|
||||||
await addToDb.run([
|
|
||||||
pull.html_url.replace('https://github.com/', '').replace(`/pull/${pull.number}`, ''),
|
|
||||||
pull.title,
|
|
||||||
pull.number,
|
|
||||||
pull.state,
|
|
||||||
pull.created_at,
|
|
||||||
pull.closed_at,
|
|
||||||
pull.merged_at,
|
|
||||||
pull.html_url,
|
|
||||||
pull.user.login,
|
|
||||||
pull.user.html_url,
|
|
||||||
'(PR)',
|
|
||||||
pull.draft,
|
|
||||||
]);
|
|
||||||
pulls++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`Fetching pull requests for ${repository} - ${pulls} * ${page}`);
|
|
||||||
|
|
||||||
page++;
|
|
||||||
if (res.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.success(`Pull requests have been fetched for ${repository} - ${pulls}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
pulls = null;
|
|
||||||
Object.freeze(pulls);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setIssue = async(issue: Issue) => {
|
|
||||||
const exists = await db.prepare(`SELECT * FROM issuesandprs WHERE number = ? AND repository = ?`).get(issue.number, issue.repository);
|
|
||||||
if (exists) {
|
|
||||||
db.exec(`UPDATE issuesandprs SET state = '${issue.state}', closed_at = '${issue.closed_at}', title = '${issue.title}' WHERE number = ${issue.number} AND repository = '${issue.repository}'`);
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error
|
|
||||||
addToDb.run([
|
|
||||||
issue.repository,
|
|
||||||
issue.title,
|
|
||||||
issue.number,
|
|
||||||
issue.state,
|
|
||||||
issue.created_at,
|
|
||||||
issue.closed_at,
|
|
||||||
null,
|
|
||||||
issue.html_url,
|
|
||||||
issue.user_login,
|
|
||||||
issue.user_html_url,
|
|
||||||
'(IS)',
|
|
||||||
null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setPullRequest = async(pull: PullRequest) => {
|
|
||||||
const exists = await db.prepare(`SELECT * FROM issuesandprs WHERE number = ? AND repository = ?`).get(pull.number, pull.repository);
|
|
||||||
if (exists) {
|
|
||||||
db.exec(`UPDATE issuesandprs SET state = '${pull.state}', closed_at = '${pull.closed_at}', merged_at = '${pull.merged_at}', title = '${pull.title}' WHERE number = ${pull.number} AND repository = '${pull.repository}'`);
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error
|
|
||||||
addToDb.run([
|
|
||||||
pull.repository,
|
|
||||||
pull.title,
|
|
||||||
pull.number,
|
|
||||||
pull.state,
|
|
||||||
pull.created_at,
|
|
||||||
pull.closed_at,
|
|
||||||
pull.merged_at,
|
|
||||||
pull.html_url,
|
|
||||||
pull.user_login,
|
|
||||||
pull.user_html_url,
|
|
||||||
'(IS)',
|
|
||||||
pull.draft,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteIssueOrPR = (number: number, repository: string) => {
|
|
||||||
db.exec(`DELETE FROM issuesandprs WHERE repository = '${repository}' AND number = ${number}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const search = async(query: string, repository: string, state: IssueState, type: IssueType): Promise<APIApplicationCommandOptionChoice[]> => {
|
|
||||||
try {
|
|
||||||
const sqliteTypePrepase = type !== '(IS|PR)' ? ` AND type = '${type}'` : '';
|
|
||||||
const arrayFiltered = state === 'all'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ?${sqliteTypePrepase}`).all(repository)
|
|
||||||
: state === 'merged'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE merged_at IS NOT NULL AND repository = ?${sqliteTypePrepase}`).all(repository)
|
|
||||||
: await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND state = ?${sqliteTypePrepase}`).all(repository, state);
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
const array = arrayFiltered.slice(0, 25);
|
|
||||||
return array.map((issueOrPr: Issue | PullRequest) => new Object({
|
|
||||||
name: `${issueOrPr.type.slice(0, -1)} #${issueOrPr.number}) ${formatEmojiStatus(issueOrPr)} ${issueOrPr.title.slice(0, 91 - issueOrPr.id.toString().length).replace(githubTitleClean, '')}`,
|
|
||||||
value: issueOrPr.number.toString()
|
|
||||||
})) as APIApplicationCommandOptionChoice[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const searcher = new MiniSearch({
|
|
||||||
fields: query.startsWith('#') ? ['number'] : ['title'],
|
|
||||||
storeFields: ['title', 'number', 'type', 'state', 'merged_at', 'draft'],
|
|
||||||
searchOptions: {
|
|
||||||
fuzzy: 3,
|
|
||||||
processTerm: term => term.toLowerCase(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
searcher.addAll(arrayFiltered);
|
|
||||||
|
|
||||||
const result = searcher.search(query);
|
|
||||||
|
|
||||||
return (result as unknown as Issue[] | PullRequest[]).slice(0, 25).map((issueOrPr: Issue | PullRequest) => new Object({
|
|
||||||
name: `${issueOrPr.type.slice(0, -1)} #${issueOrPr.number}) ${formatEmojiStatus(issueOrPr)} ${issueOrPr.title.slice(0, 91 - issueOrPr.id.toString().length).replace(githubTitleClean, '')}`,
|
|
||||||
value: issueOrPr.number.toString()
|
|
||||||
})) as APIApplicationCommandOptionChoice[]
|
|
||||||
} catch(e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getIssueOrPR = async(number: number, repository: string, state: IssueState, type: IssueType): Promise<Issue | PullRequest> => {
|
|
||||||
const sqliteTypePrepase = type !== '(IS|PR)' ? ` AND type = '${type}'` : '';
|
|
||||||
const issueOrPR = state === 'all'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ?${sqliteTypePrepase}`).get(repository, number)
|
|
||||||
: state === 'merged'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ? AND merged_at IS NOT NULL${sqliteTypePrepase}`).get(repository, number)
|
|
||||||
: await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ? AND state = ?${sqliteTypePrepase}`).get(repository, number, state);
|
|
||||||
|
|
||||||
return issueOrPR;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatStatus = (data: Issue | PullRequest) => {
|
|
||||||
let operation = '';
|
|
||||||
let timestamp = '';
|
|
||||||
switch(data.state as 'open' | 'closed' | 'all') {
|
|
||||||
case 'open':
|
|
||||||
operation = 'opened';
|
|
||||||
timestamp = `<t:${Math.floor(new Date(data.created_at).getTime() / 1000)}:R>`;
|
|
||||||
break;
|
|
||||||
case 'closed':
|
|
||||||
operation = (data as PullRequest).merged_at ? 'merged' : 'closed';
|
|
||||||
timestamp = (data as PullRequest).merged_at
|
|
||||||
? `<t:${Math.floor(new Date((data as PullRequest).merged_at).getTime() / 1000)}:R>`
|
|
||||||
: `<t:${Math.floor(new Date(data.closed_at).getTime() / 1000)}:R>`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${operation} ${timestamp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatEmojiStatus = (data: Issue | PullRequest) => {
|
|
||||||
let emoji = '';
|
|
||||||
switch(data.state as 'open' | 'closed' | 'all') {
|
|
||||||
case 'open':
|
|
||||||
emoji = (data as PullRequest).draft ? '⚫' : '🟢';
|
|
||||||
break;
|
|
||||||
case 'closed':
|
|
||||||
emoji = (data as PullRequest).merged_at ? '🟣' : '🔴';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return emoji;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { readdirSync } from 'fs';
|
|
||||||
import { basename, dirname, join } from 'path';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
|
||||||
|
|
||||||
export default async() => {
|
|
||||||
const commandsDir = join(__dirname, '..', 'commands');
|
|
||||||
for (
|
|
||||||
const command of readdirSync(commandsDir)
|
|
||||||
) {
|
|
||||||
const name = basename(command, '.ts');
|
|
||||||
Logger.info(`Loading ${name} command`);
|
|
||||||
await import(join(commandsDir, command));
|
|
||||||
Logger.success(`Command ${name} has been loaded`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export const githubTitleClean = /[`]/gi;
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { RouteBases, Routes } from 'discord-api-types/v10';
|
|
||||||
import { Commands } from '../managers/CommandManager';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
|
|
||||||
const sync = async(
|
|
||||||
clientToken: string,
|
|
||||||
clientUserId: string,
|
|
||||||
commands: Command[],
|
|
||||||
guildId?: string,
|
|
||||||
) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${RouteBases.api}${guildId ? Routes.applicationGuildCommands(clientUserId, guildId) : Routes.applicationCommands(clientUserId)}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(commands.flatMap(command => command.toJSON())),
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bot ${clientToken}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (res.ok) return Logger.success('🌍 All commands have been synchronized with discord api.');
|
|
||||||
const data = await res.json() as any;
|
|
||||||
|
|
||||||
if (res.status === 429) {
|
|
||||||
setTimeout(
|
|
||||||
() => sync(clientToken, clientUserId, commands, guildId),
|
|
||||||
data.retry_after * 1000,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Logger.error(
|
|
||||||
typeof data.code !== 'undefined' ? data.code.toString() : '',
|
|
||||||
data.message
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async(clientToken: string, clientUserId: string) => {
|
|
||||||
if (Commands.size === 0) return;
|
|
||||||
|
|
||||||
const [guild, global] = Commands.partition(
|
|
||||||
command => typeof command.guildId === 'string',
|
|
||||||
);
|
|
||||||
|
|
||||||
const guildIds = new Set(guild.map(c => c.guildId));
|
|
||||||
for await (const guildId of guildIds) {
|
|
||||||
const commands = guild.filter(item => item.guildId === guildId);
|
|
||||||
await sync(clientToken, clientUserId, [...commands.values()], guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.size > 0) await sync(clientToken, clientUserId, [...global.values()]);
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import Collection from '@discordjs/collection';
|
|
||||||
import { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import tags from '../../files/tags.toml';
|
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
keywords: string[];
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagCache: Collection<string, Tag> = new Collection();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(tags)) {
|
|
||||||
(value as Tag).content = (value as Tag).content.replaceAll('+', '`');
|
|
||||||
tagCache.set(key, value as unknown as Tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTag = <T extends boolean>(name: string, more?: T): T extends true ? APIApplicationCommandOptionChoice[] : Tag => {
|
|
||||||
if (more) {
|
|
||||||
const exactKeywords: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const keywordMatches: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const contentMatches: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const query = name.toLowerCase();
|
|
||||||
|
|
||||||
for (const [tagName, tag] of tagCache.entries()) {
|
|
||||||
const exactKeyword = tag.keywords.find((t) => t.toLowerCase() === query);
|
|
||||||
const includesKeyword = tag.keywords.find((t) => t.toLowerCase().includes(query));
|
|
||||||
const contentMatch = tag.content.toLowerCase().includes(query);
|
|
||||||
|
|
||||||
if (exactKeyword) {
|
|
||||||
exactKeywords.push({
|
|
||||||
name: `✅ ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
} else if (includesKeyword) {
|
|
||||||
keywordMatches.push({
|
|
||||||
name: `🔑 ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
} else if (contentMatch) {
|
|
||||||
contentMatches.push({
|
|
||||||
name: `📄 ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = [...exactKeywords, ...keywordMatches, ...contentMatches];
|
|
||||||
return tags as T extends true ? APIApplicationCommandOptionChoice[] : Tag;
|
|
||||||
} else {
|
|
||||||
const tag = tagCache.get(name) || tagCache.find(tag => tag.keywords.some(k => k.includes(name)));
|
|
||||||
return tag as T extends true ? APIApplicationCommandOptionChoice[] : Tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findTags = (name: string) => {
|
|
||||||
if (!name)
|
|
||||||
return [
|
|
||||||
...tagCache.map((tag, name) => new Object({
|
|
||||||
name: `🚀 ${name.replaceAll('-', ' ')}`,
|
|
||||||
value: name
|
|
||||||
})).slice(0, 25)
|
|
||||||
];
|
|
||||||
else {
|
|
||||||
const tags = getTag(name, true);
|
|
||||||
if (tags.length > 0) return tags;
|
|
||||||
else return findTags(null);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
// from https://github.com/discord/discord-interactions-js/blob/main/src/index.ts
|
|
||||||
|
|
||||||
import { sign } from 'tweetnacl';
|
|
||||||
import createHmac from 'create-hmac';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts different types to Uint8Array.
|
|
||||||
*
|
|
||||||
* @param value - Value to convert. Strings are parsed as hex.
|
|
||||||
* @param format - Format of value. Valid options: 'hex'. Defaults to utf-8.
|
|
||||||
* @returns Value in Uint8Array form.
|
|
||||||
*/
|
|
||||||
function valueToUint8Array(value: Uint8Array | ArrayBuffer | Buffer | string, format?: string): Uint8Array {
|
|
||||||
if (value == null) {
|
|
||||||
return new Uint8Array();
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
if (format === 'hex') {
|
|
||||||
const matches = value.match(/.{1,2}/g);
|
|
||||||
if (matches == null) {
|
|
||||||
throw new Error('Value is not a valid hex string');
|
|
||||||
}
|
|
||||||
const hexVal = matches.map((byte: string) => parseInt(byte, 16));
|
|
||||||
return new Uint8Array(hexVal);
|
|
||||||
} else {
|
|
||||||
return new TextEncoder().encode(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (Buffer.isBuffer(value)) {
|
|
||||||
const arrayBuffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.length);
|
|
||||||
return new Uint8Array(value);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// Runtime doesn't have Buffer
|
|
||||||
}
|
|
||||||
if (value instanceof ArrayBuffer) {
|
|
||||||
return new Uint8Array(value);
|
|
||||||
}
|
|
||||||
if (value instanceof Uint8Array) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
throw new Error('Unrecognized value type, must be one of: string, Buffer, ArrayBuffer, Uint8Array');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge two arrays.
|
|
||||||
*
|
|
||||||
* @param arr1 - First array
|
|
||||||
* @param arr2 - Second array
|
|
||||||
* @returns Concatenated arrays
|
|
||||||
*/
|
|
||||||
function concatUint8Arrays(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {
|
|
||||||
const merged = new Uint8Array(arr1.length + arr2.length);
|
|
||||||
merged.set(arr1);
|
|
||||||
merged.set(arr2, arr1.length);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a payload from Discord against its signature and key.
|
|
||||||
*
|
|
||||||
* @param rawBody - The raw payload data
|
|
||||||
* @param signature - The signature from the `X-Signature-Ed25519` header
|
|
||||||
* @param timestamp - The timestamp from the `X-Signature-Timestamp` header
|
|
||||||
* @param clientPublicKey - The public key from the Discord developer dashboard
|
|
||||||
* @returns Whether or not validation was successful
|
|
||||||
*/
|
|
||||||
export const verifyKey = (
|
|
||||||
body: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
signature: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
timestamp: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
clientPublicKey: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
): boolean => {
|
|
||||||
try {
|
|
||||||
const timestampData = valueToUint8Array(timestamp);
|
|
||||||
const bodyData = valueToUint8Array(body);
|
|
||||||
const message = concatUint8Arrays(timestampData, bodyData);
|
|
||||||
|
|
||||||
const signatureData = valueToUint8Array(signature, 'hex');
|
|
||||||
const publicKeyData = valueToUint8Array(clientPublicKey, 'hex');
|
|
||||||
return sign.detached.verify(message, signatureData, publicKeyData);
|
|
||||||
} catch (ex) {
|
|
||||||
console.error('[discord-interactions]: Invalid verifyKey parameters', ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a payload from GitHub against its signature and secret
|
|
||||||
*/
|
|
||||||
export const verifyGithubKey = (
|
|
||||||
body: string,
|
|
||||||
signature: string,
|
|
||||||
secret: string
|
|
||||||
): boolean => {
|
|
||||||
if (!body || !signature || !secret) return false;
|
|
||||||
|
|
||||||
const githubWebhooksSecret = new TextEncoder().encode(secret);
|
|
||||||
const sha256 = `sha256=${createHmac('sha256', githubWebhooksSecret).update(body).digest('hex')}`;
|
|
||||||
if (sha256 !== signature) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
// "bun-types" is the important part
|
"allowImportingTsExtensions": true,
|
||||||
"types": ["bun-types"]
|
// "bun-types" is the important part
|
||||||
}
|
"types": ["bun-types"]
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue