mirror of
https://github.com/xHyroM/bun-discord-bot.git
synced 2024-12-22 20:21:06 +01:00
feat: autocomplete for github cmd
This commit is contained in:
parent
3bef855240
commit
73ad04a894
8 changed files with 275 additions and 32 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -12,4 +12,5 @@ id = "CLIENT USER ID"
|
|||
guild_id = "GUILD ID"
|
||||
|
||||
[api]
|
||||
github_personal_access_token = ""
|
||||
github_personal_access_token = ""
|
||||
github_webhooks_secret = ""
|
|
@ -10,8 +10,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^0.7.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"discord-api-types": "^0.36.1",
|
||||
"hono": "^1.6.4",
|
||||
"minisearch": "^5.0.0",
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
}
|
|
@ -2,13 +2,10 @@ import { APIApplicationCommandInteractionDataStringOption, ApplicationCommandOpt
|
|||
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';
|
||||
import formatStatus from '../utils/formatStatus';
|
||||
import { CommandContext } from '../structures/contexts/CommandContext';
|
||||
import { getIssueOrPR, search } from '../utils/githubUtils';
|
||||
|
||||
const cooldowns: Collection<string, number> = new Collection();
|
||||
const invalidIssue = (ctx: CommandContext, query: string) => {
|
||||
|
@ -25,7 +22,10 @@ new Command({
|
|||
name: 'query',
|
||||
description: 'Issue numer/name, PR number/name or direct link to Github Issue or PR',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true
|
||||
required: true,
|
||||
run: (ctx) => {
|
||||
return ctx.respond(search(ctx.value, ctx?.options?.[1]?.value as string || 'oven-sh/bun'));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'repository',
|
||||
|
@ -64,35 +64,32 @@ new Command({
|
|||
const repositoryOwner = repositorySplit[0];
|
||||
const repositoryName = repositorySplit[1];
|
||||
|
||||
const isIssueOrPR = githubIssuesAndPullRequests(repositoryOwner, repositoryName).test(query);
|
||||
const isIssueOrPRNumber = isNumeric(query);
|
||||
|
||||
cooldowns.set(ctx.user.id, Date.now() + 30000);
|
||||
if (!isIssueOrPR && !isIssueOrPRNumber) {
|
||||
let issueOrPR = getIssueOrPR(parseInt(query), repository);
|
||||
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);
|
||||
|
||||
query = data.items[0].number;
|
||||
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)' : '(ISSUE)',
|
||||
};
|
||||
}
|
||||
|
||||
const issueUrl = `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/issues/${isIssueOrPR ? query.split('/issues/')[1] : query}`;
|
||||
|
||||
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();
|
||||
if (data.message) return invalidIssue(ctx, query);
|
||||
|
||||
return ctx.editResponse([
|
||||
`[#${data.number} ${repositoryOwner}/${repositoryName}](<${data.html_url}>) by [${data.user.login}](<${data.user.html_url}>) ${formatStatus(data)}`,
|
||||
data.title
|
||||
`[#${issueOrPR.number} ${repositoryOwner}/${repositoryName}](<${issueOrPR.html_url}>) by [${issueOrPR.user_login}](<${issueOrPR.user_html_url}>) ${formatStatus(issueOrPR)}`,
|
||||
issueOrPR.title
|
||||
].join('\n'));
|
||||
}
|
||||
})
|
70
src/index.ts
70
src/index.ts
|
@ -12,7 +12,11 @@ import { Commands } from './managers/CommandManager';
|
|||
import registerCommands from './utils/registerCommands';
|
||||
import { Option, OptionOptions } from './structures/Option';
|
||||
import { AutocompleteContext } from './structures/contexts/AutocompleteContext';
|
||||
import { deleteIssue, deletePullRequest, fetchIssues, fetchPullRequests, issues, setIssue, setPullRequest } from './utils/githubUtils';
|
||||
import createHmac from 'create-hmac';
|
||||
|
||||
await fetchIssues();
|
||||
await fetchPullRequests();
|
||||
await loadCommands();
|
||||
try {
|
||||
await registerCommands(config.client.token, config.client.id);
|
||||
|
@ -21,7 +25,7 @@ try {
|
|||
}
|
||||
|
||||
const app = new Hono();
|
||||
app.get('*', (c) => c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'));
|
||||
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');
|
||||
|
@ -56,7 +60,8 @@ app.post('/interaction', bodyParse(), async(c) => {
|
|||
return option.run(new AutocompleteContext(
|
||||
c,
|
||||
option,
|
||||
focused.value
|
||||
focused.value,
|
||||
interaction.data.options as any
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -77,6 +82,67 @@ app.post('/interaction', bodyParse(), async(c) => {
|
|||
});
|
||||
})
|
||||
|
||||
app.post('/github_webhook', bodyParse(), (c) => {
|
||||
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
|
||||
|
||||
const githubWebhooksSecret = new TextEncoder().encode(config.api.github_webhooks_secret);
|
||||
const sha256 = `sha256=${createHmac('sha256', githubWebhooksSecret).update(JSON.stringify(c.req.parsedBody)).digest('hex')}`;
|
||||
if (sha256 !== c.req.headers.get('X-Hub-Signature-256')) return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D
|
||||
|
||||
const issueOrPr = c.req.parsedBody;
|
||||
if (issueOrPr.action !== 'deleted') {
|
||||
if (issueOrPr.issue) {
|
||||
console.log('issue');
|
||||
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: new Date(issueOrPr.issue.created_at),
|
||||
closed_at: new Date(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: '(ISSUE)',
|
||||
})
|
||||
}
|
||||
else {
|
||||
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: new Date(issueOrPr.pull_request.created_at),
|
||||
closed_at: new Date(issueOrPr.pull_request.closed_at),
|
||||
merged_at: new Date(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)',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (issueOrPr.issue) deleteIssue(
|
||||
issueOrPr.issue.number, issueOrPr.issue.repository_url.replace('https://api.github.com/repos/', '')
|
||||
);
|
||||
else deletePullRequest(
|
||||
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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { APIApplicationCommandOptionChoice, InteractionResponseType } from 'discord-api-types/v10';
|
||||
import { APIApplicationCommandInteractionDataBasicOption, APIApplicationCommandOptionChoice, InteractionResponseType } from 'discord-api-types/v10';
|
||||
import { Context } from 'hono';
|
||||
import { Option, OptionOptions } from '../Option';
|
||||
|
||||
|
@ -6,11 +6,13 @@ 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) {
|
||||
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[]) {
|
||||
|
|
175
src/utils/githubUtils.ts
Normal file
175
src/utils/githubUtils.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
// @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';
|
||||
|
||||
interface Issue {
|
||||
id: number;
|
||||
repository: string;
|
||||
title: string;
|
||||
number: number;
|
||||
state: 'open' | 'closed',
|
||||
created_at: Date;
|
||||
closed_at: Date | null;
|
||||
html_url: string;
|
||||
user_login: string;
|
||||
user_html_url: string;
|
||||
type: '(ISSUE)' | '(PR)';
|
||||
}
|
||||
|
||||
interface PullRequest extends Issue {
|
||||
merged_at: Date | null;
|
||||
}
|
||||
|
||||
export let issues: Issue[] = [];
|
||||
export let pulls: PullRequest[] = [];
|
||||
|
||||
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;
|
||||
|
||||
issues.push({
|
||||
id: issue.number,
|
||||
repository: issue.repository_url.replace('https://api.github.com/repos/', ''),
|
||||
title: issue.title,
|
||||
number: issue.number,
|
||||
state: issue.state,
|
||||
created_at: new Date(issue.created_at),
|
||||
closed_at: new Date(issue.closed_at),
|
||||
html_url: issue.html_url,
|
||||
user_login: issue.user.login,
|
||||
user_html_url: issue.user.html_url,
|
||||
type: '(ISSUE)'
|
||||
})
|
||||
}
|
||||
|
||||
Logger.debug(`Fetching issues for ${repository} - ${issues.length} * ${page}`);
|
||||
|
||||
page++;
|
||||
if (res.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.success(`Issues have been fetched for ${repository} - ${issues.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
pulls.push({
|
||||
id: pull.number,
|
||||
repository: pull.html_url.replace('https://github.com/', '').replace(`/pull/${pull.number}`, ''),
|
||||
title: pull.title,
|
||||
number: pull.number,
|
||||
state: pull.state,
|
||||
created_at: new Date(pull.created_at),
|
||||
closed_at: new Date(pull.closed_at),
|
||||
merged_at: new Date(pull.merged_at),
|
||||
html_url: pull.html_url,
|
||||
user_login: pull.user.login,
|
||||
user_html_url: pull.user.html_url,
|
||||
type: '(PR)',
|
||||
})
|
||||
}
|
||||
|
||||
Logger.debug(`Fetching pull requests for ${repository} - ${pulls.length} * ${page}`);
|
||||
|
||||
page++;
|
||||
if (res.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.success(`Pull requests have been fetched for ${repository} - ${pulls.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const setIssue = (issue: Issue) => {
|
||||
const exists = issues.findIndex(i => i.number === issue.number && i.repository === issue.repository);
|
||||
if (exists >= 0) issues[exists] = issue;
|
||||
else issues.push(issue);
|
||||
}
|
||||
|
||||
export const setPullRequest = (pull: PullRequest) => {
|
||||
const exists = pulls.findIndex(i => i.number === pull.number);
|
||||
if (exists >= 0) pulls[exists] = pull;
|
||||
else pulls.push(pull);
|
||||
}
|
||||
|
||||
export const deleteIssue = (number: number, repository: string) => {
|
||||
issues = issues.filter(i => i.number === number && i.repository === repository);
|
||||
}
|
||||
|
||||
export const deletePullRequest = (number: number, repository: string) => {
|
||||
pulls = pulls.filter(p => p.number === number && p.repository === repository);
|
||||
}
|
||||
|
||||
export const search = (query: string, repository: string): APIApplicationCommandOptionChoice[] => {
|
||||
try {
|
||||
const pullsFiltered = pulls.filter(pull => pull.repository === repository);
|
||||
const issuesFiltered = issues.filter(issue => issue.repository === repository);
|
||||
|
||||
if (!query) {
|
||||
const array = [].concat(pullsFiltered.slice(0, 13), issuesFiltered.slice(0, 12));
|
||||
return array.map((issueOrPr: Issue | PullRequest) => new Object({
|
||||
name: `${issueOrPr.type} ${issueOrPr.title.slice(0, 93)}`,
|
||||
value: issueOrPr.number.toString()
|
||||
})) as APIApplicationCommandOptionChoice[]
|
||||
}
|
||||
|
||||
const array = [].concat(pullsFiltered, issuesFiltered);
|
||||
|
||||
const searcher = new MiniSearch({
|
||||
fields: ['title', 'number', 'type'],
|
||||
storeFields: ['title', 'number', 'type'],
|
||||
searchOptions: {
|
||||
fuzzy: 3,
|
||||
processTerm: term => term.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
searcher.addAll(array);
|
||||
|
||||
const result = searcher.search(query);
|
||||
|
||||
return (result as unknown as Issue[] | PullRequest[]).slice(0, 25).map((issueOrPr: Issue | PullRequest) => new Object({
|
||||
name: `${issueOrPr.type} ${issueOrPr.title.slice(0, 93).replace(/[^a-z0-9 ]/gi, '')}`,
|
||||
value: issueOrPr.number.toString()
|
||||
})) as APIApplicationCommandOptionChoice[]
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const getIssueOrPR = (number: number, repository: string): Issue | PullRequest => {
|
||||
return issues.find(issue => issue.number === number && issue.repository === repository) ||
|
||||
pulls.find(pull => pull.number === number && pull.repository === repository);
|
||||
}
|
|
@ -21,7 +21,7 @@ const sync = async(
|
|||
}
|
||||
)
|
||||
|
||||
if (res.ok) return Logger.success('🌍 All commands has been synchronized with discord api.');
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue