Add github webhook and mapping
This commit is contained in:
@@ -2,6 +2,7 @@ DISCORD_TOKEN=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_GUILD_ID=
|
||||
RELEASE_SECRET=
|
||||
LINK_SECRET=super_secret_for_url_discord
|
||||
E621_BASE_URL=https://e621.net
|
||||
E926_BASE_URL=https://e926.net
|
||||
|
||||
Generated
+2
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@redis/client": "^5.1.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"discord-oauth2": "^2.12.1",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^16.5.0",
|
||||
@@ -1792,6 +1793,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@redis/client": "^5.1.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"discord-oauth2": "^2.12.1",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^16.5.0",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ApplicationIntegrationType, BitFieldResolvable, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageFlags, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'github-mapping',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('github-mapping')
|
||||
.setDescription('Maps github users to discord ids for releases.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add a user mapping.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('github-name')
|
||||
.setDescription('The github username of the user (case sensitive).')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a user mapping.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('List all github-discord mappings.')
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
const subcommand = await interaction.options.getSubcommand(true);
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const discordUserInput = interaction.options.getString('discord-user', true);
|
||||
|
||||
const matches = mentionRegex.exec(discordUserInput);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : discordUserInput;
|
||||
|
||||
const githubName = interaction.options.getString('github-name', true);
|
||||
|
||||
const existingMappingId = await Database.getGithubFromDiscordId(idToUse);
|
||||
const existingMappingName = await Database.getDiscordIdFromGithub(githubName);
|
||||
|
||||
if (existingMappingId) return interaction.editReply(`Discord user id is already mapped to ${existingMappingId}`);
|
||||
if (existingMappingName) return interaction.editReply(`Github username is already mapped to <@${existingMappingName}> (${existingMappingName})`);
|
||||
|
||||
Database.putGithubUserMapping(idToUse, githubName);
|
||||
|
||||
return interaction.editReply('Mapping added.');
|
||||
} else if (subcommand == 'remove') {
|
||||
const discordUserInput = interaction.options.getString('discord-user', true);
|
||||
|
||||
const matches = mentionRegex.exec(discordUserInput);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : discordUserInput;
|
||||
|
||||
Database.removeGithubUserMapping(idToUse);
|
||||
return interaction.editReply('Mapping removed.');
|
||||
} else if (subcommand == 'list') {
|
||||
const allMappings = await Database.getAllGithubUserMappings();
|
||||
|
||||
return interaction.editReply(allMappings.map(m => `- <@${m.discord_id}> (${m.discord_id}) - ${m.github_username}`).join('\n'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -93,6 +93,12 @@ export default {
|
||||
.setName('remove-link-skip-channel')
|
||||
.setDescription('Remove a link skip channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('github-release-channel')
|
||||
.setDescription('Set the github release channel.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
@@ -241,6 +247,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const githubReleaseChannel = interaction.options.getChannel('github-release-channel');
|
||||
|
||||
if (githubReleaseChannel) {
|
||||
await Database.setGuildGithubReleaseChannel(interaction.guildId!, githubReleaseChannel.id);
|
||||
|
||||
response += `Github releases channel set to ${githubReleaseChannel}.\n`;
|
||||
}
|
||||
|
||||
if (response.length == 0) return interaction.editReply({ content: 'No settings provided.' });
|
||||
|
||||
interaction.editReply({ content: response });
|
||||
|
||||
+2
-1
@@ -2,13 +2,14 @@ import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, REDIS_URL, PORT } = process.env;
|
||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, REDIS_URL, PORT } = process.env;
|
||||
|
||||
export const config = {
|
||||
DISCORD_TOKEN,
|
||||
DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET,
|
||||
DISCORD_GUILD_ID,
|
||||
RELEASE_SECRET,
|
||||
LINK_SECRET,
|
||||
E621_BASE_URL,
|
||||
E926_BASE_URL,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { Client as DiscordClient, GatewayIntentBits, Guild, MessageFlags, Partia
|
||||
import { config } from './config';
|
||||
import { Handler } from './types';
|
||||
import { checkExpiredBans, initIfNecessary, loadHandlersFrom, refreshCommands } from './utils';
|
||||
import { initializeDiscordJoiner } from './discord-joiner';
|
||||
import { initializeWebserver } from './webserver';
|
||||
import { Database } from './shared/Database';
|
||||
import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events';
|
||||
import { pruneOldTickets, ticketCooldownMap } from './shared/ticket-cooldown';
|
||||
@@ -135,7 +135,7 @@ client.on('ready', async () => {
|
||||
await Database.open('./data/discord-main.db');
|
||||
await openRedisClient(config.REDIS_URL!, client);
|
||||
|
||||
await initializeDiscordJoiner();
|
||||
await initializeWebserver(client);
|
||||
|
||||
// Prune ticket cooldowns that are expired every day
|
||||
setInterval(pruneOldTickets, 8.64e+7);
|
||||
|
||||
+42
-2
@@ -3,7 +3,7 @@ import { open, Database as SqliteDatabase } from 'sqlite';
|
||||
import { config } from '../config';
|
||||
import DiscordOAuth2 from 'discord-oauth2';
|
||||
import { serializeMessage, wait } from '../utils';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting } from '../types';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping } from '../types';
|
||||
import { Message } from '../events';
|
||||
|
||||
const DB_SCHEMA = `
|
||||
@@ -27,7 +27,8 @@ const DB_SCHEMA = `
|
||||
private_help_role_id TEXT,
|
||||
staff_categories TEXT,
|
||||
safe_channels TEXT,
|
||||
link_skip_channels TEXT
|
||||
link_skip_channels TEXT,
|
||||
github_release_channel TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
@@ -68,6 +69,12 @@ const DB_SCHEMA = `
|
||||
user_id TEXT,
|
||||
expires_at datetime
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS github_user_mapping (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id TEXT,
|
||||
github_username TEXT
|
||||
);
|
||||
`;
|
||||
|
||||
export class Database {
|
||||
@@ -167,6 +174,10 @@ export class Database {
|
||||
await Database.db.run('UPDATE settings SET private_help_role_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildGithubReleaseChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET github_release_channel = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
// Since "setting" has guaranteed values and is never set by the user, this shouldn't cause any security issues.
|
||||
// But it does allow me to skip rewriting this a bunch.
|
||||
static async getGuildArraySetting(setting: GuildArraySetting, guildId: string): Promise<string[]> {
|
||||
@@ -322,4 +333,33 @@ export class Database {
|
||||
}
|
||||
|
||||
// -- END BANS --
|
||||
|
||||
// -- START GITHUB USER MAPPING --
|
||||
|
||||
// github_user_mapping
|
||||
static async putGithubUserMapping(discordId: string, githubUsername: string) {
|
||||
await Database.db.run('INSERT INTO github_user_mapping(discord_id, github_username) VALUES (?, ?)', discordId, githubUsername);
|
||||
}
|
||||
|
||||
static async getDiscordIdFromGithub(githubUsername: string): Promise<string | null> {
|
||||
const mapping = await Database.db.get<Pick<GithubUserMapping, 'discord_id'>>('SELECT discord_id FROM github_user_mapping WHERE github_username = ?', githubUsername);
|
||||
|
||||
return mapping?.discord_id ?? null;
|
||||
}
|
||||
|
||||
static async getGithubFromDiscordId(discordId: string): Promise<string | null> {
|
||||
const mapping = await Database.db.get<Pick<GithubUserMapping, 'github_username'>>('SELECT github_username FROM github_user_mapping WHERE discord_id = ?', discordId);
|
||||
|
||||
return mapping?.github_username ?? null;
|
||||
}
|
||||
|
||||
static async getAllGithubUserMappings(): Promise<GithubUserMapping[]> {
|
||||
return await Database.db.all<GithubUserMapping[]>('SELECT * from github_user_mapping');
|
||||
}
|
||||
|
||||
static async removeGithubUserMapping(discordId: string) {
|
||||
await Database.db.run('DELETE from github_user_mapping WHERE discord_id = ?', discordId);
|
||||
}
|
||||
|
||||
// -- END GITHUB USER MAPPING --
|
||||
}
|
||||
Vendored
+7
@@ -21,6 +21,7 @@ export type GuildSettings = {
|
||||
staff_categories?: string
|
||||
safe_channels?: string
|
||||
link_skip_channels?: string
|
||||
github_release_channel?: string
|
||||
}
|
||||
|
||||
export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels';
|
||||
@@ -48,4 +49,10 @@ export type Ban = {
|
||||
id: number
|
||||
user_id: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
export type GithubUserMapping = {
|
||||
id: number
|
||||
discord_id: string
|
||||
github_username: string
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
const mentionRegex = new RegExp('@([\\S]+)', 'gi');
|
||||
|
||||
export async function fixPings(body: string): Promise<string> {
|
||||
const mappings = await Database.getAllGithubUserMappings();
|
||||
|
||||
return body.replaceAll(mentionRegex, (match, username) => {
|
||||
const mapping = mappings.find(m => m.github_username == username);
|
||||
|
||||
return mapping ? `<@${mapping.discord_id}>` : match;
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import session from 'express-session';
|
||||
import MemoryStore from 'memorystore';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Client } from 'discord.js';
|
||||
import bodyParser from 'body-parser';
|
||||
import { fixPings } from '../utils/github-user-utils';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
@@ -15,6 +18,7 @@ declare module 'express-session' {
|
||||
oauthState: string;
|
||||
}
|
||||
}
|
||||
const GITHUB_REPO_ID = 852534222;//169334303;
|
||||
|
||||
const DEV_BASE_URL = `http://localhost:${config.PORT}`;
|
||||
const PROD_BASE_URL = 'https://discord.e621.net';
|
||||
@@ -72,7 +76,7 @@ async function handleInitial(req: Request, res: Response): Promise<any> {
|
||||
|
||||
const digest = crypto.createHash('sha256').update(authString).digest('hex');
|
||||
|
||||
if (hash != digest) {
|
||||
if (hash !== digest) {
|
||||
console.error(`Bad auth: ${hash} ${digest}`);
|
||||
return sendForbidden(res, 'Bad auth');
|
||||
}
|
||||
@@ -147,7 +151,38 @@ function render(res: Response, code: number, title: string = '', message: string
|
||||
res.status(code).setHeader('Content-Type', 'text/html').send(PAGE_TEMPLATE.replaceAll('{{ title }}', title).replaceAll('{{ message }}', message));
|
||||
}
|
||||
|
||||
export function initializeDiscordJoiner() {
|
||||
async function handleGithubRelease(client: Client, req: Request, res: Response): Promise<any> {
|
||||
const signature = (req.headers['x-hub-signature-256'] as string).split('=')[1];
|
||||
const computedSignature = crypto.createHmac('sha256', config.RELEASE_SECRET!).update(req.body).digest('hex');
|
||||
|
||||
if (signature !== computedSignature) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
|
||||
const data = JSON.parse(req.body);
|
||||
if (data.action != 'published' || data.repository.id != GITHUB_REPO_ID) return;
|
||||
|
||||
const settings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!settings || !settings.github_release_channel) return;
|
||||
|
||||
const channel = await client.channels.fetch(settings.github_release_channel);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const message = `## [${months[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}](<${data.release.html_url}>)\n\n${await fixPings(data.release.body)}`;
|
||||
|
||||
const sentMessage = await channel.send(message);
|
||||
await sentMessage.startThread({ name: data.release.tag_name });
|
||||
}
|
||||
|
||||
export function initializeWebserver(client: Client) {
|
||||
const app = express();
|
||||
|
||||
const Store = MemoryStore(session);
|
||||
@@ -172,6 +207,9 @@ export function initializeDiscordJoiner() {
|
||||
app.get('/', handleInitial);
|
||||
app.get('/callback', handleCallback);
|
||||
|
||||
app.use(bodyParser.raw({ type: 'application/json' }));
|
||||
app.post('/release', handleGithubRelease.bind(null, client));
|
||||
|
||||
app.listen(config.PORT, (error) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
Reference in New Issue
Block a user