Merge branch 'master' into scheduler
This commit is contained in:
+3
-3
@@ -1,4 +1,4 @@
|
||||
/.git
|
||||
/data
|
||||
/dist
|
||||
/.git
|
||||
/data
|
||||
/dist
|
||||
/node_modules
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
data/*
|
||||
node_modules
|
||||
dist
|
||||
data/*
|
||||
.env
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"files.eol": "\n"
|
||||
}
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ----------
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
USER node
|
||||
CMD ["node", "./dist/index.js"]
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ----------
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
USER node
|
||||
CMD ["node", "./dist/index.js"]
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
## Bot Setup
|
||||
|
||||
### Prerequisites
|
||||
* Latest version of Docker ([download](https://docs.docker.com/get-docker))
|
||||
* Latest version of Docker Compose ([download](https://docs.docker.com/compose/install))
|
||||
* Git ([download](https://git-scm.com/downloads))
|
||||
* An [e621ng](https://github.com/e621ng/e621ng) instance ready to start
|
||||
* A [discord bot application](https://discord.com/developers/applications)
|
||||
|
||||
If you are on Windows Docker Compose is already included, you do not need to install it yourself.
|
||||
If you are on Linux/MacOS you can probably use your package manager.
|
||||
|
||||
### Discord application setup
|
||||
1. Create a new application
|
||||
2. Under the "Installation" sidebar
|
||||
- For "Installation Contexts" select "Guild Install"
|
||||
- Set the "Install Link" dropdown to "None"
|
||||
3. Under the "OAuth2" sidebar
|
||||
- Add a redirect to `http://localhost:8000/callback`, or where ever your discord bot joiner will be listening
|
||||
4. Under the "Bot" sidebar
|
||||
- It is recommended to disable "Public Bot"
|
||||
- Enable "Server Members Intent"
|
||||
- Enable "Message Content Intent"
|
||||
5. Join the bot to the desired server using the following invite link, but replace the client id with your application's id: https://discord.com/oauth2/authorize?client_id=YOUR_APPLICATION_ID_HERE&scope=bot&permissions=395271335063
|
||||
|
||||
### Configuration
|
||||
1. Copy the `.env.sample` file and rename it to `.env`
|
||||
2. Enter your bot's token (found on the `Bot` page of the application setup)
|
||||
3. Enter your application's client secret (found on the `OAuth2` page of the application setup)
|
||||
4. Enter your bot's client id (application id, found on the `General Information` page of the application setup)
|
||||
5. Enter your discord guild (server) id
|
||||
6. Enter your discord joiner link secret (found in e621ng's `docker-compose.yml` file, or other environment file, it's passed as `DANBOORU_DISCORD_SECRET`. It defaults to `super_secret_for_url_discord`)
|
||||
7. e621/e926 base url can be the same in development environments, but should be your e621ng's domain. Do not add trailing slashes
|
||||
8. Enter the redis url of your e621ng instance. You may need to expose the port from docker manually in development enviornments. This can be done by adding a `ports` mapping to the `redis` service in e621ng's `docker-compose.yml` file. You should map `6379:6379`
|
||||
9. Enter your desired port number. It is recommended to leave this at `8000`, if you select anything different you will need to map the correct port in `docker-compose.yml`
|
||||
|
||||
### Installing dependencies
|
||||
Run `npm i` to install all node dependencies. This is required to start the bot.
|
||||
|
||||
### Starting the bot
|
||||
|
||||
e621ng must be up for the bot to start properly and open the connection to the redis database.
|
||||
|
||||
#### In development
|
||||
You can use `npm run dev` to run the typescript without compiling. This will watch for changes and restart when they happen.
|
||||
|
||||
If desired, you can also build the typescript using `npm run build` and then subsequently run it using `npm run start`
|
||||
|
||||
#### In docker
|
||||
1. Run `docker compose build` to build the image
|
||||
## Bot Setup
|
||||
|
||||
### Prerequisites
|
||||
* Latest version of Docker ([download](https://docs.docker.com/get-docker))
|
||||
* Latest version of Docker Compose ([download](https://docs.docker.com/compose/install))
|
||||
* Git ([download](https://git-scm.com/downloads))
|
||||
* An [e621ng](https://github.com/e621ng/e621ng) instance ready to start
|
||||
* A [discord bot application](https://discord.com/developers/applications)
|
||||
|
||||
If you are on Windows Docker Compose is already included, you do not need to install it yourself.
|
||||
If you are on Linux/MacOS you can probably use your package manager.
|
||||
|
||||
### Discord application setup
|
||||
1. Create a new application
|
||||
2. Under the "Installation" sidebar
|
||||
- For "Installation Contexts" select "Guild Install"
|
||||
- Set the "Install Link" dropdown to "None"
|
||||
3. Under the "OAuth2" sidebar
|
||||
- Add a redirect to `http://localhost:8000/callback`, or where ever your discord bot joiner will be listening
|
||||
4. Under the "Bot" sidebar
|
||||
- It is recommended to disable "Public Bot"
|
||||
- Enable "Server Members Intent"
|
||||
- Enable "Message Content Intent"
|
||||
5. Join the bot to the desired server using the following invite link, but replace the client id with your application's id: https://discord.com/oauth2/authorize?client_id=YOUR_APPLICATION_ID_HERE&scope=bot&permissions=395271335063
|
||||
|
||||
### Configuration
|
||||
1. Copy the `.env.sample` file and rename it to `.env`
|
||||
2. Enter your bot's token (found on the `Bot` page of the application setup)
|
||||
3. Enter your application's client secret (found on the `OAuth2` page of the application setup)
|
||||
4. Enter your bot's client id (application id, found on the `General Information` page of the application setup)
|
||||
5. Enter your discord guild (server) id
|
||||
6. Enter your discord joiner link secret (found in e621ng's `docker-compose.yml` file, or other environment file, it's passed as `DANBOORU_DISCORD_SECRET`. It defaults to `super_secret_for_url_discord`)
|
||||
7. e621/e926 base url can be the same in development environments, but should be your e621ng's domain. Do not add trailing slashes
|
||||
8. Enter the redis url of your e621ng instance. You may need to expose the port from docker manually in development enviornments. This can be done by adding a `ports` mapping to the `redis` service in e621ng's `docker-compose.yml` file. You should map `6379:6379`
|
||||
9. Enter your desired port number. It is recommended to leave this at `8000`, if you select anything different you will need to map the correct port in `docker-compose.yml`
|
||||
|
||||
### Installing dependencies
|
||||
Run `npm i` to install all node dependencies. This is required to start the bot.
|
||||
|
||||
### Starting the bot
|
||||
|
||||
e621ng must be up for the bot to start properly and open the connection to the redis database.
|
||||
|
||||
#### In development
|
||||
You can use `npm run dev` to run the typescript without compiling. This will watch for changes and restart when they happen.
|
||||
|
||||
If desired, you can also build the typescript using `npm run build` and then subsequently run it using `npm run start`
|
||||
|
||||
#### In docker
|
||||
1. Run `docker compose build` to build the image
|
||||
2. Run `docker compose up` to start the bot. Changes to any files, including `.env` will require a rebuild
|
||||
+9
-9
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
hexerade:
|
||||
restart: unless-stopped
|
||||
build: .
|
||||
env_file: '.env'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
services:
|
||||
hexerade:
|
||||
restart: unless-stopped
|
||||
build: .
|
||||
env_file: '.env'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
||||
+67
-67
@@ -1,68 +1,68 @@
|
||||
const eslint = require('@eslint/js');
|
||||
const globals = require('globals');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const stylistic = require('@stylistic/eslint-plugin');
|
||||
|
||||
const ignores = ['dist/**/*', 'node_modules/**/*', 'eslint.config.js'];
|
||||
|
||||
module.exports = tseslint.config(
|
||||
{
|
||||
ignores,
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended
|
||||
]
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
ignores,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-empty': 'off',
|
||||
'prefer-const': ['error'],
|
||||
'no-async-promise-executor': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'quotes': ['error', 'single', { 'avoidEscape': true }],
|
||||
'semi': ['error'],
|
||||
'@stylistic/indent': ['error', 2, { 'SwitchCase': 1 }],
|
||||
'@stylistic/arrow-parens': ['error', 'as-needed', { 'requireForBlockBody': true }],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/block-spacing': ['error'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'@stylistic/comma-dangle': ['error', {
|
||||
'arrays': 'only-multiline',
|
||||
'objects': 'only-multiline'
|
||||
}],
|
||||
'@stylistic/comma-spacing': ['error'],
|
||||
'@stylistic/dot-location': ['error', 'property'],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
'@stylistic/keyword-spacing': ['error'],
|
||||
'@stylistic/key-spacing': ['error'],
|
||||
'@stylistic/no-trailing-spaces': ['error'],
|
||||
'@stylistic/no-whitespace-before-property': ['error'],
|
||||
'@stylistic/object-curly-newline': ['error', {
|
||||
'multiline': true,
|
||||
'consistent': true
|
||||
}],
|
||||
'@stylistic/operator-linebreak': ['error', 'before'],
|
||||
'@stylistic/space-infix-ops': ['error']
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores,
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
},
|
||||
}
|
||||
const eslint = require('@eslint/js');
|
||||
const globals = require('globals');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const stylistic = require('@stylistic/eslint-plugin');
|
||||
|
||||
const ignores = ['dist/**/*', 'node_modules/**/*', 'eslint.config.js'];
|
||||
|
||||
module.exports = tseslint.config(
|
||||
{
|
||||
ignores,
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended
|
||||
]
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
ignores,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-empty': 'off',
|
||||
'prefer-const': ['error'],
|
||||
'no-async-promise-executor': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'quotes': ['error', 'single', { 'avoidEscape': true }],
|
||||
'semi': ['error'],
|
||||
'@stylistic/indent': ['error', 2, { 'SwitchCase': 1 }],
|
||||
'@stylistic/arrow-parens': ['error', 'as-needed', { 'requireForBlockBody': true }],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/block-spacing': ['error'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'@stylistic/comma-dangle': ['error', {
|
||||
'arrays': 'only-multiline',
|
||||
'objects': 'only-multiline'
|
||||
}],
|
||||
'@stylistic/comma-spacing': ['error'],
|
||||
'@stylistic/dot-location': ['error', 'property'],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
'@stylistic/keyword-spacing': ['error'],
|
||||
'@stylistic/key-spacing': ['error'],
|
||||
'@stylistic/no-trailing-spaces': ['error'],
|
||||
'@stylistic/no-whitespace-before-property': ['error'],
|
||||
'@stylistic/object-curly-newline': ['error', {
|
||||
'multiline': true,
|
||||
'consistent': true
|
||||
}],
|
||||
'@stylistic/operator-linebreak': ['error', 'before'],
|
||||
'@stylistic/space-infix-ops': ['error']
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores,
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
},
|
||||
}
|
||||
);
|
||||
+41
-41
@@ -1,42 +1,42 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'claim-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' });
|
||||
|
||||
if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' });
|
||||
|
||||
if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to claim tickets.' });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to claim ticket.' });
|
||||
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const unclaimButton = new ButtonBuilder()
|
||||
.setCustomId('unclaim-ticket')
|
||||
.setLabel('Unclaim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, unclaimButton);
|
||||
|
||||
await interaction.message.edit({
|
||||
content: `${interaction.message.content}\n\nClaimed by: ${interaction.user}`,
|
||||
components: [row]
|
||||
});
|
||||
|
||||
await interaction.reply({ content: `Ticket claimed by ${interaction.user}.` });
|
||||
}
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'claim-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' });
|
||||
|
||||
if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' });
|
||||
|
||||
if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to claim tickets.' });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to claim ticket.' });
|
||||
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const unclaimButton = new ButtonBuilder()
|
||||
.setCustomId('unclaim-ticket')
|
||||
.setLabel('Unclaim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, unclaimButton);
|
||||
|
||||
await interaction.message.edit({
|
||||
content: `${interaction.message.content}\n\nClaimed by: ${interaction.user}`,
|
||||
components: [row]
|
||||
});
|
||||
|
||||
await interaction.reply({ content: `Ticket claimed by ${interaction.user}.` });
|
||||
}
|
||||
};
|
||||
@@ -1,31 +1,31 @@
|
||||
import { ButtonInteraction, Client, MessageFlags, ChannelType, PermissionFlagsBits } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: 'close-mod-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
const channel = await interaction.channel?.fetch();
|
||||
const guild = await interaction.guild?.fetch();
|
||||
const member = await guild?.members.fetch(interaction.user.id);
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread || !member)
|
||||
return interaction.editReply({ content: 'Oops. Something went wrong. Please report this to a staff member.' });
|
||||
|
||||
if (!member.permissions.has(PermissionFlagsBits.KickMembers))
|
||||
return interaction.editReply({ content: 'Only staff members may close mod tickets.' });
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content,
|
||||
components: []
|
||||
});
|
||||
|
||||
await channel.send('This ticket has been closed by staff.');
|
||||
|
||||
await interaction.editReply({ content: 'Ticket closed.' });
|
||||
|
||||
channel.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
import { ButtonInteraction, Client, MessageFlags, ChannelType, PermissionFlagsBits } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: 'close-mod-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
const channel = await interaction.channel?.fetch();
|
||||
const guild = await interaction.guild?.fetch();
|
||||
const member = await guild?.members.fetch(interaction.user.id);
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread || !member)
|
||||
return interaction.editReply({ content: 'Oops. Something went wrong. Please report this to a staff member.' });
|
||||
|
||||
if (!member.permissions.has(PermissionFlagsBits.KickMembers))
|
||||
return interaction.editReply({ content: 'Only staff members may close mod tickets.' });
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content,
|
||||
components: []
|
||||
});
|
||||
|
||||
await channel.send('This ticket has been closed by staff.');
|
||||
|
||||
await interaction.editReply({ content: 'Ticket closed.' });
|
||||
|
||||
channel.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
};
|
||||
+27
-27
@@ -1,28 +1,28 @@
|
||||
import { ButtonInteraction, Client, MessageFlags, ChannelType } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'close-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong. Please report this to a staff member.' });
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content,
|
||||
components: []
|
||||
});
|
||||
|
||||
await Database.closePrivateHelpTicket(channel.id);
|
||||
|
||||
await channel.send(`This ticket has been closed by ${interaction.user}`);
|
||||
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Ticket closed.' });
|
||||
|
||||
channel.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
import { ButtonInteraction, Client, MessageFlags, ChannelType } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'close-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong. Please report this to a staff member.' });
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content,
|
||||
components: []
|
||||
});
|
||||
|
||||
await Database.closePrivateHelpTicket(channel.id);
|
||||
|
||||
await channel.send(`This ticket has been closed by ${interaction.user}`);
|
||||
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Ticket closed.' });
|
||||
|
||||
channel.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
};
|
||||
+22
-22
@@ -1,23 +1,23 @@
|
||||
import { ButtonInteraction, Client, MessageFlags } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'dev-watch',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member) return await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'There was an error. Please try again later.' });
|
||||
|
||||
const settings = await Database.getGuildSettings(interaction.guild!.id);
|
||||
|
||||
if (!settings || !settings.devwatch_role_id) return await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'There was an error. Please try again later.' });
|
||||
|
||||
if (member.roles.cache.has(settings.devwatch_role_id)) {
|
||||
await member.roles.remove(settings.devwatch_role_id);
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Removed role.' });
|
||||
} else {
|
||||
await member.roles.add(settings.devwatch_role_id);
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Added role.' });
|
||||
}
|
||||
}
|
||||
import { ButtonInteraction, Client, MessageFlags } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'dev-watch',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member) return await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'There was an error. Please try again later.' });
|
||||
|
||||
const settings = await Database.getGuildSettings(interaction.guild!.id);
|
||||
|
||||
if (!settings || !settings.devwatch_role_id) return await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'There was an error. Please try again later.' });
|
||||
|
||||
if (member.roles.cache.has(settings.devwatch_role_id)) {
|
||||
await member.roles.remove(settings.devwatch_role_id);
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Removed role.' });
|
||||
} else {
|
||||
await member.roles.add(settings.devwatch_role_id);
|
||||
await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Added role.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
+14
-14
@@ -1,15 +1,15 @@
|
||||
import { ButtonInteraction, Client } from 'discord.js';
|
||||
import { getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'note-next',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getNoteMessage(userId, parseInt(page) + 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
import { ButtonInteraction, Client } from 'discord.js';
|
||||
import { getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'note-next',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getNoteMessage(userId, parseInt(page) + 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ButtonInteraction, Client} from 'discord.js';
|
||||
import { getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'note-previous',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getNoteMessage(userId, parseInt(page) - 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
import { ButtonInteraction, Client} from 'discord.js';
|
||||
import { getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'note-previous',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getNoteMessage(userId, parseInt(page) - 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -1,40 +1,40 @@
|
||||
import { ButtonInteraction, Client, MessageFlags, MessageMentions } from 'discord.js';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'open-ticket-for-reported-message',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const message = await interaction.message.fetch();
|
||||
const guild = await interaction.guild!.fetch();
|
||||
|
||||
const reportEmbed = message.embeds[0]!;
|
||||
|
||||
const regex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
const reportedMessageUrl = reportEmbed.fields[0].value;
|
||||
const reporterId = regex.exec(reportEmbed.fields[2].value)!.groups!.id;
|
||||
const additionalInfo = reportEmbed.fields[3]?.name == 'Additional Information' ? reportEmbed.fields[3].value : '';
|
||||
|
||||
const reporter = await guild.members.fetch(reporterId) ?? await client.users.fetch(reporterId);
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, null, `Ticket creted by staff in response to message report: ${message.url}. Reported message: ${reportedMessageUrl}. ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`, `Ticket: Message Report From ${reporter.displayName}`, [reporterId, interaction.user.id]);
|
||||
|
||||
if (thread) {
|
||||
await interaction.reply({
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
content: `Ticket created: ${thread}`
|
||||
});
|
||||
|
||||
const requestIndex = reportEmbed.fields.findIndex(f => f.name == 'User Requested Private Ticket');
|
||||
if (requestIndex != -1) reportEmbed.fields.splice(requestIndex, 1);
|
||||
|
||||
reportEmbed.fields.push({
|
||||
name: 'Private Help Ticket',
|
||||
value: thread.url,
|
||||
inline: false
|
||||
});
|
||||
|
||||
await message.edit({ embeds: [reportEmbed], components: [] });
|
||||
}
|
||||
}
|
||||
import { ButtonInteraction, Client, MessageFlags, MessageMentions } from 'discord.js';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'open-ticket-for-reported-message',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const message = await interaction.message.fetch();
|
||||
const guild = await interaction.guild!.fetch();
|
||||
|
||||
const reportEmbed = message.embeds[0]!;
|
||||
|
||||
const regex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
const reportedMessageUrl = reportEmbed.fields[0].value;
|
||||
const reporterId = regex.exec(reportEmbed.fields[2].value)!.groups!.id;
|
||||
const additionalInfo = reportEmbed.fields[3]?.name == 'Additional Information' ? reportEmbed.fields[3].value : '';
|
||||
|
||||
const reporter = await guild.members.fetch(reporterId) ?? await client.users.fetch(reporterId);
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, null, `Ticket creted by staff in response to message report: ${message.url}. Reported message: ${reportedMessageUrl}. ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`, `Ticket: Message Report From ${reporter.displayName}`, [reporterId, interaction.user.id]);
|
||||
|
||||
if (thread) {
|
||||
await interaction.reply({
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
content: `Ticket created: ${thread}`
|
||||
});
|
||||
|
||||
const requestIndex = reportEmbed.fields.findIndex(f => f.name == 'User Requested Private Ticket');
|
||||
if (requestIndex != -1) reportEmbed.fields.splice(requestIndex, 1);
|
||||
|
||||
reportEmbed.fields.push({
|
||||
name: 'Private Help Ticket',
|
||||
value: thread.url,
|
||||
inline: false
|
||||
});
|
||||
|
||||
await message.edit({ embeds: [reportEmbed], components: [] });
|
||||
}
|
||||
}
|
||||
};
|
||||
+19
-19
@@ -1,20 +1,20 @@
|
||||
import { ButtonInteraction, Client, ModalBuilder, TextInputStyle, MessageFlags } from 'discord.js';
|
||||
import { canOpenPrivateHelpTicket, createTextInput } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'private-help',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
if (!(await canOpenPrivateHelpTicket(interaction.user.id)))
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You can only have one open ticket at a time that is less than a day old.' });
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('open-ticket-modal')
|
||||
.setTitle('Get in contact');
|
||||
|
||||
const label = createTextInput('ticket-message', 'What is the reason for your ticket?', null, true, TextInputStyle.Paragraph, 1500, 10);
|
||||
|
||||
modal.addLabelComponents(label);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
import { ButtonInteraction, Client, ModalBuilder, TextInputStyle, MessageFlags } from 'discord.js';
|
||||
import { canOpenPrivateHelpTicket, createTextInput } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'private-help',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
if (!(await canOpenPrivateHelpTicket(interaction.user.id)))
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You can only have one open ticket at a time that is less than a day old.' });
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('open-ticket-modal')
|
||||
.setTitle('Get in contact');
|
||||
|
||||
const label = createTextInput('ticket-message', 'What is the reason for your ticket?', null, true, TextInputStyle.Paragraph, 1500, 10);
|
||||
|
||||
modal.addLabelComponents(label);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
};
|
||||
+14
-14
@@ -1,15 +1,15 @@
|
||||
import { ButtonInteraction, Client } from 'discord.js';
|
||||
import { getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'records-next',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getRecordMessageFromDiscordId(userId, parseInt(page) + 1, interaction.guild!);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
import { ButtonInteraction, Client } from 'discord.js';
|
||||
import { getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'records-next',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getRecordMessageFromDiscordId(userId, parseInt(page) + 1, interaction.guild!);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ButtonInteraction, Client} from 'discord.js';
|
||||
import { getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'records-previous',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getRecordMessageFromDiscordId(userId, parseInt(page) - 1, interaction.guild!);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
import { ButtonInteraction, Client} from 'discord.js';
|
||||
import { getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'records-previous',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getRecordMessageFromDiscordId(userId, parseInt(page) - 1, interaction.guild!);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -1,44 +1,44 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'unclaim-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' });
|
||||
|
||||
if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' });
|
||||
|
||||
if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to unclaim this ticket.' });
|
||||
|
||||
if (!interaction.message.content.split('\n').at(-1)!.includes(`<@${interaction.user.id}>`)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You did not claim this ticket.' });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to unclaim ticket.' });
|
||||
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId('claim-ticket')
|
||||
.setLabel('Claim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, claimButton);
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content.split('\n').slice(0, -1).join('\n').trim(),
|
||||
components: [row]
|
||||
});
|
||||
|
||||
await interaction.reply({ content: 'Ticket unclaimed.', flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'unclaim-ticket',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction) {
|
||||
const channel = await interaction.channel?.fetch();
|
||||
|
||||
if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' });
|
||||
|
||||
if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' });
|
||||
|
||||
if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to unclaim this ticket.' });
|
||||
|
||||
if (!interaction.message.content.split('\n').at(-1)!.includes(`<@${interaction.user.id}>`)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You did not claim this ticket.' });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to unclaim ticket.' });
|
||||
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId('claim-ticket')
|
||||
.setLabel('Claim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, claimButton);
|
||||
|
||||
await interaction.message.edit({
|
||||
content: interaction.message.content.split('\n').slice(0, -1).join('\n').trim(),
|
||||
components: [row]
|
||||
});
|
||||
|
||||
await interaction.reply({ content: 'Ticket unclaimed.', flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
};
|
||||
+140
-140
@@ -1,141 +1,141 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'ban',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ban')
|
||||
.setDescription('Bans a user.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to ban.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the ban')
|
||||
.setRequired(false)
|
||||
.setMaxLength(400)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('hours')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('minutes')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('seconds')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('delete-message-days')
|
||||
.setDescription('How far back to delete messages (in days, default: 0 days).')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(7)
|
||||
)
|
||||
.addBooleanOption(option =>
|
||||
option
|
||||
.setName('full-ban')
|
||||
.setDescription('Whether or not to prevent the user from joining on known alts (and ban all existing alts).')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
|
||||
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
const reason = interaction.options.getString('reason') ?? '';
|
||||
|
||||
const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6;
|
||||
const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000;
|
||||
const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000;
|
||||
|
||||
const duration = hours + minutes + seconds;
|
||||
|
||||
const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400;
|
||||
|
||||
const fullBan = interaction.options.getBoolean('full-ban') ?? false;
|
||||
|
||||
let banMember: GuildMember | null = null;
|
||||
|
||||
try {
|
||||
banMember = await interaction.guild.members.fetch(idToUse);
|
||||
} catch (e) {
|
||||
// Member not in server.
|
||||
}
|
||||
|
||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
|
||||
return await interaction.editReply('You do not have permission to ban this user.');
|
||||
}
|
||||
|
||||
if (banMember && !banMember.bannable) {
|
||||
return await interaction.editReply('I do not have permission to ban this user.');
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + duration);
|
||||
|
||||
await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan);
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.create(idToUse, {
|
||||
reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(),
|
||||
deleteMessageSeconds: deleteMessageDays
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error banning user (couldn't ban).");
|
||||
}
|
||||
|
||||
if (fullBan) {
|
||||
const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild);
|
||||
|
||||
await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt);
|
||||
}
|
||||
|
||||
await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`);
|
||||
}
|
||||
};
|
||||
|
||||
async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) {
|
||||
for (const data of altData) {
|
||||
if (data.type == 'discord') {
|
||||
try {
|
||||
if (!data.banned) {
|
||||
await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt);
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'ban',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ban')
|
||||
.setDescription('Bans a user.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to ban.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the ban')
|
||||
.setRequired(false)
|
||||
.setMaxLength(400)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('hours')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('minutes')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('seconds')
|
||||
.setDescription('The duration of the ban, added with other options (0 for permanent).')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('delete-message-days')
|
||||
.setDescription('How far back to delete messages (in days, default: 0 days).')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(7)
|
||||
)
|
||||
.addBooleanOption(option =>
|
||||
option
|
||||
.setName('full-ban')
|
||||
.setDescription('Whether or not to prevent the user from joining on known alts (and ban all existing alts).')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
|
||||
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
const reason = interaction.options.getString('reason') ?? '';
|
||||
|
||||
const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6;
|
||||
const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000;
|
||||
const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000;
|
||||
|
||||
const duration = hours + minutes + seconds;
|
||||
|
||||
const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400;
|
||||
|
||||
const fullBan = interaction.options.getBoolean('full-ban') ?? false;
|
||||
|
||||
let banMember: GuildMember | null = null;
|
||||
|
||||
try {
|
||||
banMember = await interaction.guild.members.fetch(idToUse);
|
||||
} catch (e) {
|
||||
// Member not in server.
|
||||
}
|
||||
|
||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
|
||||
return await interaction.editReply('You do not have permission to ban this user.');
|
||||
}
|
||||
|
||||
if (banMember && !banMember.bannable) {
|
||||
return await interaction.editReply('I do not have permission to ban this user.');
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + duration);
|
||||
|
||||
await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan);
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.create(idToUse, {
|
||||
reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(),
|
||||
deleteMessageSeconds: deleteMessageDays
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error banning user (couldn't ban).");
|
||||
}
|
||||
|
||||
if (fullBan) {
|
||||
const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild);
|
||||
|
||||
await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt);
|
||||
}
|
||||
|
||||
await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`);
|
||||
}
|
||||
};
|
||||
|
||||
async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) {
|
||||
for (const data of altData) {
|
||||
if (data.type == 'discord') {
|
||||
try {
|
||||
if (!data.banned) {
|
||||
await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt);
|
||||
}
|
||||
}
|
||||
+47
-47
@@ -1,48 +1,48 @@
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { KnowledgebaseItem } from '../types';
|
||||
|
||||
export default {
|
||||
name: 'cite',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('cite')
|
||||
.setDescription('Cite content from the knowledgebase.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const item = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!item) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
return interaction.editReply(item.content);
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
if (!interaction.guild) return interaction.respond([]);
|
||||
|
||||
const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const toRespond = items.filter(i => !value ? true : i.content.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.name,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { KnowledgebaseItem } from '../types';
|
||||
|
||||
export default {
|
||||
name: 'cite',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('cite')
|
||||
.setDescription('Cite content from the knowledgebase.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const item = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!item) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
return interaction.editReply(item.content);
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
if (!interaction.guild) return interaction.respond([]);
|
||||
|
||||
const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const toRespond = items.filter(i => !value ? true : i.content.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.name,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
};
|
||||
+41
-41
@@ -1,42 +1,42 @@
|
||||
import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: 'devwatch',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('devwatch')
|
||||
.setDescription('Sends a dev watch role toggle button.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('content')
|
||||
.setDescription('The content of the message.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('button-label')
|
||||
.setDescription('The button label.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.channel || !interaction.channel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' });
|
||||
|
||||
const content = interaction.options.getString('content') ?? '';
|
||||
const label = interaction.options.getString('button-label') ?? 'Toggle DevWatch Role';
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId('dev-watch')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(label);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(button);
|
||||
|
||||
await interaction.channel.send({ components: [row], content });
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
|
||||
}
|
||||
import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: 'devwatch',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('devwatch')
|
||||
.setDescription('Sends a dev watch role toggle button.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('content')
|
||||
.setDescription('The content of the message.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('button-label')
|
||||
.setDescription('The button label.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.channel || !interaction.channel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' });
|
||||
|
||||
const content = interaction.options.getString('content') ?? '';
|
||||
const label = interaction.options.getString('button-label') ?? 'Toggle DevWatch Role';
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId('dev-watch')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(label);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(button);
|
||||
|
||||
await interaction.channel.send({ components: [row], content });
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
|
||||
}
|
||||
};
|
||||
+41
-41
@@ -1,42 +1,42 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { deferInteraction, getDiscordAlts, getE621User } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'finduser',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('finduser')
|
||||
.setDescription("Find a user's discord account based on their e621 usernamename or id.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The e621 username or e621 id to find the discord user of.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = interaction.options.getString('user', true);
|
||||
|
||||
try {
|
||||
const e621User = await getE621User(user);
|
||||
|
||||
if (!e621User) {
|
||||
return interaction.editReply('I got lost along the way. Who again?');
|
||||
}
|
||||
|
||||
const content = await getDiscordAlts(e621User.id, interaction.guild, 1, [e621User.id]);
|
||||
|
||||
interaction.editReply(`[${e621User.name}](${config.E621_BASE_URL}/users/${e621User.id})<${e621User.id}>'s e621 and discord account(s):\n${content}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
interaction.editReply('I got lost in the net.');
|
||||
}
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { deferInteraction, getDiscordAlts, getE621User } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'finduser',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('finduser')
|
||||
.setDescription("Find a user's discord account based on their e621 usernamename or id.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The e621 username or e621 id to find the discord user of.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = interaction.options.getString('user', true);
|
||||
|
||||
try {
|
||||
const e621User = await getE621User(user);
|
||||
|
||||
if (!e621User) {
|
||||
return interaction.editReply('I got lost along the way. Who again?');
|
||||
}
|
||||
|
||||
const content = await getDiscordAlts(e621User.id, interaction.guild, 1, [e621User.id]);
|
||||
|
||||
interaction.editReply(`[${e621User.name}](${config.E621_BASE_URL}/users/${e621User.id})<${e621User.id}>'s e621 and discord account(s):\n${content}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
interaction.editReply('I got lost in the net.');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,87 +1,87 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
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'));
|
||||
}
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
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'));
|
||||
}
|
||||
}
|
||||
};
|
||||
+126
-126
@@ -1,127 +1,127 @@
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, ModalBuilder, PermissionFlagsBits, SlashCommandBuilder, TextInputStyle } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { KnowledgebaseItem } from '../types';
|
||||
import { createTextInput, deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'knowledgebase',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('knowledgebase')
|
||||
.setDescription('Access the compendium of knowledge.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add to the knowledgebase.')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Purge knowledge from the universe.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('edit')
|
||||
.setDescription('Edit a knowledgebase entry.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry to edit.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('add-knowledgebase-item-modal')
|
||||
.setTitle('Add to knowledgebase');
|
||||
|
||||
const nameLabel = createTextInput('name', 'Knowledgebase Item Name', null, true, TextInputStyle.Short, 300, 1);
|
||||
const contentLabel = createTextInput('content', 'Item Content', null, true, TextInputStyle.Paragraph, 2000, 1);
|
||||
|
||||
modal.addLabelComponents(nameLabel, contentLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
} else if (subcommand == 'remove') {
|
||||
await deferInteraction(interaction);
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const item = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!item) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Removed',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: item.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
value: item.content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.removeFromKnowledgebase(id);
|
||||
|
||||
return interaction.editReply(`Removed knowledgebase entry \`${item.name}\`.`);
|
||||
} else if (subcommand == 'edit') {
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!existingItem) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`edit-knowledgebase-item-modal_${id}`)
|
||||
.setTitle(`Editing knowledgebase item ${existingItem.name.slice(0, 18)}`);
|
||||
|
||||
const contentLabel = createTextInput('content', 'New Content', null, true, TextInputStyle.Paragraph, 2000, 1);
|
||||
|
||||
modal.addLabelComponents(contentLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
if (!interaction.guild) return interaction.respond([]);
|
||||
|
||||
const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const toRespond = items.filter(i => !value ? true : i.name.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.name,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, ModalBuilder, PermissionFlagsBits, SlashCommandBuilder, TextInputStyle } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { KnowledgebaseItem } from '../types';
|
||||
import { createTextInput, deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'knowledgebase',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('knowledgebase')
|
||||
.setDescription('Access the compendium of knowledge.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add to the knowledgebase.')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Purge knowledge from the universe.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('edit')
|
||||
.setDescription('Edit a knowledgebase entry.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('name')
|
||||
.setDescription('The name of the entry to edit.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('add-knowledgebase-item-modal')
|
||||
.setTitle('Add to knowledgebase');
|
||||
|
||||
const nameLabel = createTextInput('name', 'Knowledgebase Item Name', null, true, TextInputStyle.Short, 300, 1);
|
||||
const contentLabel = createTextInput('content', 'Item Content', null, true, TextInputStyle.Paragraph, 2000, 1);
|
||||
|
||||
modal.addLabelComponents(nameLabel, contentLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
} else if (subcommand == 'remove') {
|
||||
await deferInteraction(interaction);
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const item = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!item) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Removed',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: item.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
value: item.content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.removeFromKnowledgebase(id);
|
||||
|
||||
return interaction.editReply(`Removed knowledgebase entry \`${item.name}\`.`);
|
||||
} else if (subcommand == 'edit') {
|
||||
const id = interaction.options.getInteger('name', true);
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!existingItem) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`edit-knowledgebase-item-modal_${id}`)
|
||||
.setTitle(`Editing knowledgebase item ${existingItem.name.slice(0, 18)}`);
|
||||
|
||||
const contentLabel = createTextInput('content', 'New Content', null, true, TextInputStyle.Paragraph, 2000, 1);
|
||||
|
||||
modal.addLabelComponents(contentLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
if (!interaction.guild) return interaction.respond([]);
|
||||
|
||||
const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const toRespond = items.filter(i => !value ? true : i.name.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.name,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
};
|
||||
+133
-133
@@ -1,134 +1,134 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { logCustomEvent, resolveUser } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'link',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('link')
|
||||
.setDescription('Manually link discord users and e621 users.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('create')
|
||||
.setDescription('Create a link.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('e621-id')
|
||||
.setDescription('The id of the e621 user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a link.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('e621-id')
|
||||
.setDescription('The id of the e621 user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
const subcommand = await interaction.options.getSubcommand(true);
|
||||
|
||||
const discordUserInput = interaction.options.getString('discord-user', true);
|
||||
|
||||
const matches = mentionRegex.exec(discordUserInput);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : discordUserInput;
|
||||
|
||||
const user = await resolveUser(client, idToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
const e621Id = interaction.options.getInteger('e621-id', true);
|
||||
|
||||
if (subcommand == 'create') {
|
||||
const existingLinks = await Database.getDiscordIds(e621Id);
|
||||
|
||||
if (existingLinks.includes(user.id)) return interaction.editReply('Accounts already linked.');
|
||||
|
||||
await Database.putUser(e621Id, user);
|
||||
|
||||
await logCustomEvent(interaction.guild!, {
|
||||
title: 'Account Link Created',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Admin',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Discord User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'E621 User',
|
||||
value: `${config.E621_BASE_URL}/users/${e621Id}`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.editReply('Accounts linked');
|
||||
} else if (subcommand == 'remove') {
|
||||
const existingLinks = await Database.getDiscordIds(e621Id);
|
||||
|
||||
if (!existingLinks.includes(user.id)) return interaction.editReply('Accounts not linked.');
|
||||
|
||||
await Database.removeUser(e621Id, user.id);
|
||||
|
||||
await logCustomEvent(interaction.guild!, {
|
||||
title: 'Account Link Removed',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Admin',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Discord User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'E621 User',
|
||||
value: `${config.E621_BASE_URL}/users/${e621Id}`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.editReply('Accounts unlinked');
|
||||
}
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { logCustomEvent, resolveUser } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'link',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('link')
|
||||
.setDescription('Manually link discord users and e621 users.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('create')
|
||||
.setDescription('Create a link.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('e621-id')
|
||||
.setDescription('The id of the e621 user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a link.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('discord-user')
|
||||
.setDescription('The discord user id, or mention, of the user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('e621-id')
|
||||
.setDescription('The id of the e621 user.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
const subcommand = await interaction.options.getSubcommand(true);
|
||||
|
||||
const discordUserInput = interaction.options.getString('discord-user', true);
|
||||
|
||||
const matches = mentionRegex.exec(discordUserInput);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : discordUserInput;
|
||||
|
||||
const user = await resolveUser(client, idToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
const e621Id = interaction.options.getInteger('e621-id', true);
|
||||
|
||||
if (subcommand == 'create') {
|
||||
const existingLinks = await Database.getDiscordIds(e621Id);
|
||||
|
||||
if (existingLinks.includes(user.id)) return interaction.editReply('Accounts already linked.');
|
||||
|
||||
await Database.putUser(e621Id, user);
|
||||
|
||||
await logCustomEvent(interaction.guild!, {
|
||||
title: 'Account Link Created',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Admin',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Discord User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'E621 User',
|
||||
value: `${config.E621_BASE_URL}/users/${e621Id}`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.editReply('Accounts linked');
|
||||
} else if (subcommand == 'remove') {
|
||||
const existingLinks = await Database.getDiscordIds(e621Id);
|
||||
|
||||
if (!existingLinks.includes(user.id)) return interaction.editReply('Accounts not linked.');
|
||||
|
||||
await Database.removeUser(e621Id, user.id);
|
||||
|
||||
await logCustomEvent(interaction.guild!, {
|
||||
title: 'Account Link Removed',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Admin',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Discord User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'E621 User',
|
||||
value: `${config.E621_BASE_URL}/users/${e621Id}`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.editReply('Accounts unlinked');
|
||||
}
|
||||
}
|
||||
};
|
||||
+27
-27
@@ -1,28 +1,28 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { openModTicketModal } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'mod-ticket',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('mod-ticket')
|
||||
.setDescription('Opens a mod private ticket and pulls the user into it.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to pull in to the ticket.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server.');
|
||||
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const member = await interaction.guild.members.fetch(user.id);
|
||||
|
||||
if (!member) return interaction.editReply('Could not find member.');
|
||||
|
||||
openModTicketModal(interaction, member);
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { openModTicketModal } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'mod-ticket',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('mod-ticket')
|
||||
.setDescription('Opens a mod private ticket and pulls the user into it.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to pull in to the ticket.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server.');
|
||||
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const member = await interaction.guild.members.fetch(user.id);
|
||||
|
||||
if (!member) return interaction.editReply('Could not find member.');
|
||||
|
||||
openModTicketModal(interaction, member);
|
||||
}
|
||||
};
|
||||
+35
-35
@@ -1,36 +1,36 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'name-sync',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('name-sync')
|
||||
.setDescription('Sync your discord nickname to your e621 name.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('id')
|
||||
.setDescription('The id of the e621 user to sync your nickname to.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
const id = interaction.options.getInteger('id');
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member || !guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, id);
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, SlashCommandBuilder } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'name-sync',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('name-sync')
|
||||
.setDescription('Sync your discord nickname to your e621 name.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('id')
|
||||
.setDescription('The id of the e621 user to sync your nickname to.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
const id = interaction.options.getInteger('id');
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member || !guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, id);
|
||||
}
|
||||
};
|
||||
+238
-238
@@ -1,239 +1,239 @@
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent, resolveUser } from '../utils';
|
||||
import { getNoteMessage } from '../utils/note-utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'notes',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('notes')
|
||||
.setDescription('Add, view, or remove user notes.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add notes to a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to add a note to.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the note.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('edit')
|
||||
.setDescription('Edit notes on a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to edit the notes of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('note')
|
||||
.setDescription('The note to edit.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('new-reason')
|
||||
.setDescription('The new reason for the note.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove notes from a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to remove a note from.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('note')
|
||||
.setDescription('The note to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription("List a user's notes")
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to list the notes of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const user = await resolveUser(client, idToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const reason = interaction.options.getString('reason', true);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: reason,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.putNote(user.id, reason, interaction.user.id);
|
||||
|
||||
interaction.editReply(`Note added to <@${user.id}> (\`${user.username}\` | \`${user.id}\`).\n\nReason:\n${reason}`);
|
||||
} else if (subcommand == 'remove') {
|
||||
const noteId = interaction.options.getInteger('note', true);
|
||||
|
||||
const notes = await Database.getNotes(user.id);
|
||||
const note = notes.find(n => n.id == noteId);
|
||||
|
||||
if (!note) return interaction.editReply('Note not found.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Removed',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: `${note.reason}\nBy: <@${note.mod_id}>`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.removeNote(noteId);
|
||||
interaction.editReply('Removed note.');
|
||||
} else if (subcommand == 'edit') {
|
||||
const noteId = interaction.options.getInteger('note', true);
|
||||
|
||||
const notes = await Database.getNotes(user.id);
|
||||
const note = notes.find(n => n.id == noteId);
|
||||
|
||||
if (!note) return interaction.editReply('Note not found.');
|
||||
|
||||
const reason = interaction.options.getString('new-reason', true);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Edited',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Old reason',
|
||||
value: note.reason
|
||||
},
|
||||
{
|
||||
name: 'New reason',
|
||||
value: reason,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.editNote(noteId, note.reason, reason, interaction.user.id);
|
||||
|
||||
interaction.editReply(`Note on <@${user.id}> (\`${user.username}\` | \`${user.id}\`) edited.\n\nNew reason:\n${reason}`);
|
||||
} else if (subcommand == 'list') {
|
||||
const noteMessage = await getNoteMessage(user.id, 1);
|
||||
|
||||
if (!noteMessage) return interaction.editReply(`No notes found for <@${user.id}> (\`${user.username}\` | \`${user.id}\`)`);
|
||||
|
||||
interaction.editReply(noteMessage);
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
if (!idToUse) return interaction.respond([]);
|
||||
|
||||
const value = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
const notes = await Database.getNotes(idToUse);
|
||||
|
||||
const toRespond = notes.filter(w => !value ? true : w.reason.toLowerCase().includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map((w) => {
|
||||
return {
|
||||
name: w.reason.substring(0, 50),
|
||||
value: w.id
|
||||
};
|
||||
}));
|
||||
}
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent, resolveUser } from '../utils';
|
||||
import { getNoteMessage } from '../utils/note-utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'notes',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('notes')
|
||||
.setDescription('Add, view, or remove user notes.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add notes to a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to add a note to.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the note.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('edit')
|
||||
.setDescription('Edit notes on a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to edit the notes of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('note')
|
||||
.setDescription('The note to edit.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('new-reason')
|
||||
.setDescription('The new reason for the note.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove notes from a user.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to remove a note from.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('note')
|
||||
.setDescription('The note to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription("List a user's notes")
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to list the notes of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const user = await resolveUser(client, idToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const reason = interaction.options.getString('reason', true);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: reason,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.putNote(user.id, reason, interaction.user.id);
|
||||
|
||||
interaction.editReply(`Note added to <@${user.id}> (\`${user.username}\` | \`${user.id}\`).\n\nReason:\n${reason}`);
|
||||
} else if (subcommand == 'remove') {
|
||||
const noteId = interaction.options.getInteger('note', true);
|
||||
|
||||
const notes = await Database.getNotes(user.id);
|
||||
const note = notes.find(n => n.id == noteId);
|
||||
|
||||
if (!note) return interaction.editReply('Note not found.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Removed',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: `${note.reason}\nBy: <@${note.mod_id}>`,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.removeNote(noteId);
|
||||
interaction.editReply('Removed note.');
|
||||
} else if (subcommand == 'edit') {
|
||||
const noteId = interaction.options.getInteger('note', true);
|
||||
|
||||
const notes = await Database.getNotes(user.id);
|
||||
const note = notes.find(n => n.id == noteId);
|
||||
|
||||
if (!note) return interaction.editReply('Note not found.');
|
||||
|
||||
const reason = interaction.options.getString('new-reason', true);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Edited',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Old reason',
|
||||
value: note.reason
|
||||
},
|
||||
{
|
||||
name: 'New reason',
|
||||
value: reason,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.editNote(noteId, note.reason, reason, interaction.user.id);
|
||||
|
||||
interaction.editReply(`Note on <@${user.id}> (\`${user.username}\` | \`${user.id}\`) edited.\n\nNew reason:\n${reason}`);
|
||||
} else if (subcommand == 'list') {
|
||||
const noteMessage = await getNoteMessage(user.id, 1);
|
||||
|
||||
if (!noteMessage) return interaction.editReply(`No notes found for <@${user.id}> (\`${user.username}\` | \`${user.id}\`)`);
|
||||
|
||||
interaction.editReply(noteMessage);
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const idToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
if (!idToUse) return interaction.respond([]);
|
||||
|
||||
const value = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
const notes = await Database.getNotes(idToUse);
|
||||
|
||||
const toRespond = notes.filter(w => !value ? true : w.reason.toLowerCase().includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map((w) => {
|
||||
return {
|
||||
name: w.reason.substring(0, 50),
|
||||
value: w.id
|
||||
};
|
||||
}));
|
||||
}
|
||||
};
|
||||
+251
-251
@@ -1,252 +1,252 @@
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, User } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { TicketPhrase } from '../types';
|
||||
import { logCustomEvent } from '../utils';
|
||||
|
||||
const MIN_PHRASE_LENGTH = 1;
|
||||
const MAX_PHRASE_LENGTH = 512;
|
||||
|
||||
type SubcommandGroup = 'admin' | 'personal';
|
||||
type Subcommand = 'add' | 'remove' | 'list' | 'dump' | 'purge';
|
||||
|
||||
export default {
|
||||
name: 'phrases',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('phrases')
|
||||
.setDescription('Manage notified phrases.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addSubcommandGroup(subcommandGroup =>
|
||||
subcommandGroup
|
||||
.setName('admin')
|
||||
.setDescription('Manage admin notified phrases.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add an admin notification phrase.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to add.')
|
||||
.setRequired(true)
|
||||
.setMinLength(MIN_PHRASE_LENGTH)
|
||||
.setMaxLength(MAX_PHRASE_LENGTH)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove an admin notification phrase.')
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Get a list of the current admin notification phrases.')
|
||||
)
|
||||
)
|
||||
.addSubcommandGroup(subcommandGroup =>
|
||||
subcommandGroup
|
||||
.setName('personal')
|
||||
.setDescription('Manage personal notified phrases.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add a personal notification phrase.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to add.')
|
||||
.setRequired(true)
|
||||
.setMinLength(MIN_PHRASE_LENGTH)
|
||||
.setMaxLength(MAX_PHRASE_LENGTH)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a personal notification phrase.')
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Get a list of the current personal notification phrases.')
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('dump')
|
||||
.setDescription('List all notification phrases.')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('purge')
|
||||
.setDescription("Purge a user's phrases.")
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to purge the phrases of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const subcommandGroup: SubcommandGroup | null = interaction.options.getSubcommandGroup() as SubcommandGroup;
|
||||
const subcommand: Subcommand | null = interaction.options.getSubcommand() as Subcommand;
|
||||
|
||||
switch (subcommand) {
|
||||
case 'add':
|
||||
return addPhrase(interaction, interaction.options.getString('phrase', true), subcommandGroup!);
|
||||
case 'remove':
|
||||
return removePhrase(interaction, interaction.options.getNumber('phrase', true), subcommandGroup!);
|
||||
case 'list':
|
||||
return listPhrases(interaction, subcommandGroup!);
|
||||
case 'dump':
|
||||
return dumpPhrases(interaction);
|
||||
case 'purge':
|
||||
return purgePhrases(interaction, interaction.options.getUser('user', true));
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
const subcommandGroup: SubcommandGroup | null = interaction.options.getSubcommandGroup() as SubcommandGroup;
|
||||
|
||||
if (!subcommandGroup) return interaction.respond([]);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const phrases: TicketPhrase[] = await Database.getTicketPhrasesFor(subcommandGroup == 'admin' ? 'admin' : interaction.user.id);
|
||||
|
||||
const toRespond = phrases.filter(p => !value ? true : p.phrase.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.phrase,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
};
|
||||
|
||||
async function purgePhrases(interaction: ChatInputCommandInteraction, user: User) {
|
||||
const phrases: TicketPhrase[] = await Database.getTicketPhrasesFor(user.id);
|
||||
|
||||
const count = await Database.removeAllTicketPhrasesFor(user.id);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Ticket Phrases Purged',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Target User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Count',
|
||||
value: count.toString(),
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Purged the following phrases (${count}):\n${phrases.map(p => `- \`${p.phrase}\``).join('\n')}`);
|
||||
}
|
||||
|
||||
async function dumpPhrases(interaction: ChatInputCommandInteraction) {
|
||||
let content = '';
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
await Database.getAllTicketPhrases((phrase: TicketPhrase) => {
|
||||
if (phrase.user_id == 'admin' && (!guildSettings || !guildSettings.admin_role_id)) return;
|
||||
|
||||
const mention = phrase.user_id == 'admin' ? `<@&${guildSettings?.admin_role_id}>` : `<@${phrase.user_id}>`;
|
||||
|
||||
content += `${mention}: \`${phrase.phrase}\`\n`;
|
||||
});
|
||||
|
||||
if (content.length == 0) return interaction.reply('No phrases found.');
|
||||
|
||||
interaction.reply('The following phrases are registered:\n\n' + content);
|
||||
}
|
||||
|
||||
async function addPhrase(interaction: ChatInputCommandInteraction, phrase: string, group: SubcommandGroup) {
|
||||
await Database.putTicketPhrase(group == 'admin' ? 'admin' : interaction.user.id, phrase);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: `${group == 'admin' ? 'Admin ' : ''}Ticket Phrase Added`,
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Phrase',
|
||||
value: phrase,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Phrases matching "${phrase}" will now alert ${group == 'admin' ? 'admins' : 'you'}.`);
|
||||
}
|
||||
|
||||
async function removePhrase(interaction: ChatInputCommandInteraction, phraseId: number, group: SubcommandGroup) {
|
||||
const phrase = await Database.getTicketPhrase(phraseId);
|
||||
|
||||
if (!phrase) return interaction.reply('Phrase not found');
|
||||
|
||||
await Database.removeTicketPhrase(phraseId);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: `${group == 'admin' ? 'Admin ' : ''}Ticket Phrase Removed`,
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Phrase',
|
||||
value: phrase.phrase,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Phrase will no longer alert ${group == 'admin' ? 'admins' : 'you'}.`);
|
||||
}
|
||||
|
||||
async function listPhrases(interaction: ChatInputCommandInteraction, group: SubcommandGroup) {
|
||||
const phrases = await Database.getTicketPhrasesFor(group == 'admin' ? 'admin' : interaction.user.id);
|
||||
|
||||
if (phrases.length == 0) return interaction.reply('No phrases registered');
|
||||
|
||||
interaction.reply(`The following phrases are registered:\n\n${phrases.map(p => (`- \`${p.phrase}\``)).join('\n')}`);
|
||||
import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, User } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { TicketPhrase } from '../types';
|
||||
import { logCustomEvent } from '../utils';
|
||||
|
||||
const MIN_PHRASE_LENGTH = 1;
|
||||
const MAX_PHRASE_LENGTH = 512;
|
||||
|
||||
type SubcommandGroup = 'admin' | 'personal';
|
||||
type Subcommand = 'add' | 'remove' | 'list' | 'dump' | 'purge';
|
||||
|
||||
export default {
|
||||
name: 'phrases',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('phrases')
|
||||
.setDescription('Manage notified phrases.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addSubcommandGroup(subcommandGroup =>
|
||||
subcommandGroup
|
||||
.setName('admin')
|
||||
.setDescription('Manage admin notified phrases.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add an admin notification phrase.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to add.')
|
||||
.setRequired(true)
|
||||
.setMinLength(MIN_PHRASE_LENGTH)
|
||||
.setMaxLength(MAX_PHRASE_LENGTH)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove an admin notification phrase.')
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Get a list of the current admin notification phrases.')
|
||||
)
|
||||
)
|
||||
.addSubcommandGroup(subcommandGroup =>
|
||||
subcommandGroup
|
||||
.setName('personal')
|
||||
.setDescription('Manage personal notified phrases.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add a personal notification phrase.')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to add.')
|
||||
.setRequired(true)
|
||||
.setMinLength(MIN_PHRASE_LENGTH)
|
||||
.setMaxLength(MAX_PHRASE_LENGTH)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a personal notification phrase.')
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('phrase')
|
||||
.setDescription('The phrase to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Get a list of the current personal notification phrases.')
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('dump')
|
||||
.setDescription('List all notification phrases.')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('purge')
|
||||
.setDescription("Purge a user's phrases.")
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to purge the phrases of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const subcommandGroup: SubcommandGroup | null = interaction.options.getSubcommandGroup() as SubcommandGroup;
|
||||
const subcommand: Subcommand | null = interaction.options.getSubcommand() as Subcommand;
|
||||
|
||||
switch (subcommand) {
|
||||
case 'add':
|
||||
return addPhrase(interaction, interaction.options.getString('phrase', true), subcommandGroup!);
|
||||
case 'remove':
|
||||
return removePhrase(interaction, interaction.options.getNumber('phrase', true), subcommandGroup!);
|
||||
case 'list':
|
||||
return listPhrases(interaction, subcommandGroup!);
|
||||
case 'dump':
|
||||
return dumpPhrases(interaction);
|
||||
case 'purge':
|
||||
return purgePhrases(interaction, interaction.options.getUser('user', true));
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
const subcommandGroup: SubcommandGroup | null = interaction.options.getSubcommandGroup() as SubcommandGroup;
|
||||
|
||||
if (!subcommandGroup) return interaction.respond([]);
|
||||
|
||||
const value = interaction.options.getFocused();
|
||||
|
||||
const phrases: TicketPhrase[] = await Database.getTicketPhrasesFor(subcommandGroup == 'admin' ? 'admin' : interaction.user.id);
|
||||
|
||||
const toRespond = phrases.filter(p => !value ? true : p.phrase.includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map(p => ({
|
||||
name: p.phrase,
|
||||
value: p.id
|
||||
})));
|
||||
}
|
||||
};
|
||||
|
||||
async function purgePhrases(interaction: ChatInputCommandInteraction, user: User) {
|
||||
const phrases: TicketPhrase[] = await Database.getTicketPhrasesFor(user.id);
|
||||
|
||||
const count = await Database.removeAllTicketPhrasesFor(user.id);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Ticket Phrases Purged',
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Target User',
|
||||
value: `<@${user.id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Count',
|
||||
value: count.toString(),
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Purged the following phrases (${count}):\n${phrases.map(p => `- \`${p.phrase}\``).join('\n')}`);
|
||||
}
|
||||
|
||||
async function dumpPhrases(interaction: ChatInputCommandInteraction) {
|
||||
let content = '';
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
await Database.getAllTicketPhrases((phrase: TicketPhrase) => {
|
||||
if (phrase.user_id == 'admin' && (!guildSettings || !guildSettings.admin_role_id)) return;
|
||||
|
||||
const mention = phrase.user_id == 'admin' ? `<@&${guildSettings?.admin_role_id}>` : `<@${phrase.user_id}>`;
|
||||
|
||||
content += `${mention}: \`${phrase.phrase}\`\n`;
|
||||
});
|
||||
|
||||
if (content.length == 0) return interaction.reply('No phrases found.');
|
||||
|
||||
interaction.reply('The following phrases are registered:\n\n' + content);
|
||||
}
|
||||
|
||||
async function addPhrase(interaction: ChatInputCommandInteraction, phrase: string, group: SubcommandGroup) {
|
||||
await Database.putTicketPhrase(group == 'admin' ? 'admin' : interaction.user.id, phrase);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: `${group == 'admin' ? 'Admin ' : ''}Ticket Phrase Added`,
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Phrase',
|
||||
value: phrase,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Phrases matching "${phrase}" will now alert ${group == 'admin' ? 'admins' : 'you'}.`);
|
||||
}
|
||||
|
||||
async function removePhrase(interaction: ChatInputCommandInteraction, phraseId: number, group: SubcommandGroup) {
|
||||
const phrase = await Database.getTicketPhrase(phraseId);
|
||||
|
||||
if (!phrase) return interaction.reply('Phrase not found');
|
||||
|
||||
await Database.removeTicketPhrase(phraseId);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: `${group == 'admin' ? 'Admin ' : ''}Ticket Phrase Removed`,
|
||||
description: null,
|
||||
color: 0xFF0000,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Phrase',
|
||||
value: phrase.phrase,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
interaction.reply(`Phrase will no longer alert ${group == 'admin' ? 'admins' : 'you'}.`);
|
||||
}
|
||||
|
||||
async function listPhrases(interaction: ChatInputCommandInteraction, group: SubcommandGroup) {
|
||||
const phrases = await Database.getTicketPhrasesFor(group == 'admin' ? 'admin' : interaction.user.id);
|
||||
|
||||
if (phrases.length == 0) return interaction.reply('No phrases registered');
|
||||
|
||||
interaction.reply(`The following phrases are registered:\n\n${phrases.map(p => (`- \`${p.phrase}\``)).join('\n')}`);
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'private-help',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('private-help')
|
||||
.setDescription('Setup a private help button.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('content')
|
||||
.setDescription('The content of the message.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('button-label')
|
||||
.setDescription('The button label.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.channel || !interaction.channel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' });
|
||||
|
||||
const content = interaction.options.getString('content') ?? '';
|
||||
const label = interaction.options.getString('button-label') ?? 'Get in contact';
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId('private-help')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(label);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(button);
|
||||
|
||||
await interaction.channel.send({ components: [row], content });
|
||||
|
||||
await Database.setPrivateHelpChannel(interaction.guildId!, interaction.channelId);
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
|
||||
}
|
||||
import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'private-help',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('private-help')
|
||||
.setDescription('Setup a private help button.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('content')
|
||||
.setDescription('The content of the message.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('button-label')
|
||||
.setDescription('The button label.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.channel || !interaction.channel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' });
|
||||
|
||||
const content = interaction.options.getString('content') ?? '';
|
||||
const label = interaction.options.getString('button-label') ?? 'Get in contact';
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId('private-help')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(label);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(button);
|
||||
|
||||
await interaction.channel.send({ components: [row], content });
|
||||
|
||||
await Database.setPrivateHelpChannel(interaction.guildId!, interaction.channelId);
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
|
||||
}
|
||||
};
|
||||
+44
-44
@@ -1,45 +1,45 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { deferInteraction } from '../utils';
|
||||
import { getRecordMessageFromDiscordId } from '../utils/record-utils';
|
||||
|
||||
export default {
|
||||
name: 'records',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('records')
|
||||
.setDescription("Get a user's on-site records.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user to find the e621 user of.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('id')
|
||||
.setDescription('The discord user id to find the e621 user of.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
const id = interaction.options.getString('id');
|
||||
|
||||
if (!user && !id) {
|
||||
return interaction.reply({ content: 'No user or id given.', flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
|
||||
const idToUse = (user?.id ?? id) as string;
|
||||
|
||||
const recordMessage = await getRecordMessageFromDiscordId(idToUse, 1, interaction.guild);
|
||||
|
||||
if (!recordMessage) return interaction.editReply('No records found on any linked accounts.');
|
||||
|
||||
interaction.editReply(recordMessage);
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { deferInteraction } from '../utils';
|
||||
import { getRecordMessageFromDiscordId } from '../utils/record-utils';
|
||||
|
||||
export default {
|
||||
name: 'records',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('records')
|
||||
.setDescription("Get a user's on-site records.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user to find the e621 user of.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('id')
|
||||
.setDescription('The discord user id to find the e621 user of.')
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
const id = interaction.options.getString('id');
|
||||
|
||||
if (!user && !id) {
|
||||
return interaction.reply({ content: 'No user or id given.', flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
|
||||
const idToUse = (user?.id ?? id) as string;
|
||||
|
||||
const recordMessage = await getRecordMessageFromDiscordId(idToUse, 1, interaction.guild);
|
||||
|
||||
if (!recordMessage) return interaction.editReply('No records found on any linked accounts.');
|
||||
|
||||
interaction.editReply(recordMessage);
|
||||
}
|
||||
};
|
||||
+50
-50
@@ -1,51 +1,51 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, RateLimitError, SlashCommandBuilder } from 'discord.js';
|
||||
import { msToHuman } from '../utils';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('rename')
|
||||
.setDescription('Rename the general channel.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('new-name')
|
||||
.setDescription('The new name of the general channel.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!guildSettings || !guildSettings.general_chat_id) {
|
||||
return interaction.reply('No general chat id found.');
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('new-name', true);
|
||||
|
||||
if (name.length > 100) {
|
||||
return interaction.reply('Name must be less than 100 characters in length.');
|
||||
}
|
||||
|
||||
const channel = await interaction.guild!.channels.fetch(guildSettings.general_chat_id)!;
|
||||
|
||||
if (!channel) {
|
||||
return interaction.reply('No general chat id found.');
|
||||
}
|
||||
|
||||
try {
|
||||
await channel.setName(name);
|
||||
|
||||
interaction.reply(`Renamed general to ${channel.name}`);
|
||||
} catch (e: any) {
|
||||
if (e instanceof RateLimitError) {
|
||||
return interaction.reply(`Name change limited. Try again in ${msToHuman(e.retryAfter)}`);
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
return interaction.reply('An error has occurred.');
|
||||
}
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, RateLimitError, SlashCommandBuilder } from 'discord.js';
|
||||
import { msToHuman } from '../utils';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('rename')
|
||||
.setDescription('Rename the general channel.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('new-name')
|
||||
.setDescription('The new name of the general channel.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!guildSettings || !guildSettings.general_chat_id) {
|
||||
return interaction.reply('No general chat id found.');
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('new-name', true);
|
||||
|
||||
if (name.length > 100) {
|
||||
return interaction.reply('Name must be less than 100 characters in length.');
|
||||
}
|
||||
|
||||
const channel = await interaction.guild!.channels.fetch(guildSettings.general_chat_id)!;
|
||||
|
||||
if (!channel) {
|
||||
return interaction.reply('No general chat id found.');
|
||||
}
|
||||
|
||||
try {
|
||||
await channel.setName(name);
|
||||
|
||||
interaction.reply(`Renamed general to ${channel.name}`);
|
||||
} catch (e: any) {
|
||||
if (e instanceof RateLimitError) {
|
||||
return interaction.reply(`Name change limited. Try again in ${msToHuman(e.retryAfter)}`);
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
return interaction.reply('An error has occurred.');
|
||||
}
|
||||
}
|
||||
};
|
||||
+302
-302
@@ -1,303 +1,303 @@
|
||||
import { ApplicationIntegrationType, ChannelType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('settings')
|
||||
.setDescription('Change server settings.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('general-channel')
|
||||
.setDescription('Set the general channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('tickets-channel')
|
||||
.setDescription('Set the ticket logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('event-logs-channel')
|
||||
.setDescription('Set the event logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('discord-logs-channel')
|
||||
.setDescription('Set the discord logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('audit-logs-channel')
|
||||
.setDescription('Set the audit logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('voice-logs-channel')
|
||||
.setDescription('Set the voice logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('new-member-channel')
|
||||
.setDescription('Set the new member logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('moderator-channel')
|
||||
.setDescription('Set the site moderator channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('admin-role')
|
||||
.setDescription('Set the admin role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('private-helper-role')
|
||||
.setDescription('Set the private helper role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('devwatch-role')
|
||||
.setDescription('Set the DevWatch role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-staff-category')
|
||||
.setDescription('Add a category to staff categories.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('remove-staff-category')
|
||||
.setDescription('Remove a category from staff categories.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-safe-channel')
|
||||
.setDescription('Add a SFW channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('remove-safe-channel')
|
||||
.setDescription('Remove a SFW channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-link-skip-channel')
|
||||
.setDescription('Add a link skip channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.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] });
|
||||
|
||||
let response = '';
|
||||
|
||||
const settings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!settings) {
|
||||
await Database.putGuild(interaction.guildId!);
|
||||
}
|
||||
|
||||
const generalChannel = interaction.options.getChannel('general-channel');
|
||||
|
||||
if (generalChannel) {
|
||||
await Database.setGuildGeneralChatId(interaction.guildId!, generalChannel.id);
|
||||
|
||||
response += `General channel set to ${generalChannel}.\n`;
|
||||
}
|
||||
|
||||
const ticketsChannel = interaction.options.getChannel('tickets-channel');
|
||||
|
||||
if (ticketsChannel) {
|
||||
await Database.setGuildTicketsLogsChannelId(interaction.guildId!, ticketsChannel.id);
|
||||
|
||||
response += `Tickets logs channel set to ${ticketsChannel}.\n`;
|
||||
}
|
||||
|
||||
const eventLogsChannel = interaction.options.getChannel('event-logs-channel');
|
||||
|
||||
if (eventLogsChannel) {
|
||||
await Database.setGuildEventsLogsChannelId(interaction.guildId!, eventLogsChannel.id);
|
||||
|
||||
response += `Event logs channel set to ${eventLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const discordLogsChannel = interaction.options.getChannel('discord-logs-channel');
|
||||
|
||||
if (discordLogsChannel) {
|
||||
await Database.setGuildDiscordLogsChannelId(interaction.guildId!, discordLogsChannel.id);
|
||||
|
||||
response += `Discord logs channel set to ${discordLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const auditLogsChannel = interaction.options.getChannel('audit-logs-channel');
|
||||
|
||||
if (auditLogsChannel) {
|
||||
await Database.setGuildAuditLogsChannelId(interaction.guildId!, auditLogsChannel.id);
|
||||
|
||||
response += `Audit logs channel set to ${auditLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const voiceLogsChannel = interaction.options.getChannel('voice-logs-channel');
|
||||
|
||||
if (voiceLogsChannel) {
|
||||
await Database.setGuildVoiceLogsChannelId(interaction.guildId!, voiceLogsChannel.id);
|
||||
|
||||
response += `Voice logs channel set to ${voiceLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const newMemberLogsChannel = interaction.options.getChannel('new-member-channel');
|
||||
|
||||
if (newMemberLogsChannel) {
|
||||
await Database.setGuildNewMemberLogsChannel(interaction.guildId!, newMemberLogsChannel.id);
|
||||
|
||||
response += `New member logs channel set to ${newMemberLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const moderatorChannel = interaction.options.getChannel('moderator-channel');
|
||||
|
||||
if (moderatorChannel) {
|
||||
await Database.setGuildModeratorChannel(interaction.guildId!, moderatorChannel.id);
|
||||
|
||||
response += `Moderator channel set to ${moderatorChannel}.\n`;
|
||||
}
|
||||
|
||||
const adminRole = interaction.options.getRole('admin-role');
|
||||
|
||||
if (adminRole) {
|
||||
await Database.setGuildAdminRole(interaction.guildId!, adminRole.id);
|
||||
|
||||
response += `Admin role set to ${adminRole}.\n`;
|
||||
}
|
||||
|
||||
const privateHelperRole = interaction.options.getRole('private-helper-role');
|
||||
|
||||
if (privateHelperRole) {
|
||||
await Database.setGuildPrivateHelperRole(interaction.guildId!, privateHelperRole.id);
|
||||
|
||||
response += `Private helper role set to ${privateHelperRole}.\n`;
|
||||
}
|
||||
|
||||
const devWatchRole = interaction.options.getRole('devwatch-role');
|
||||
|
||||
if (devWatchRole) {
|
||||
await Database.setGuildDevWatchRole(interaction.guildId!, devWatchRole.id);
|
||||
|
||||
response += `DevWatch role set to ${devWatchRole}.\n`;
|
||||
}
|
||||
|
||||
const addCategory = interaction.options.getChannel('add-staff-category');
|
||||
if (addCategory) {
|
||||
if (addCategory.type == ChannelType.GuildCategory) {
|
||||
await Database.putGuildArraySetting('staff_categories', interaction.guildId!, addCategory.id);
|
||||
|
||||
response += `Added ${addCategory.toString()} as a staff category.\n`;
|
||||
} else {
|
||||
response += `Error adding staff category: ${addCategory.toString()} isn't a category.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeCategory = interaction.options.getChannel('remove-staff-category');
|
||||
if (removeCategory) {
|
||||
if (removeCategory.type == ChannelType.GuildCategory) {
|
||||
if (await Database.removeGuildArraySetting('staff_categories', interaction.guildId!, removeCategory.id)) {
|
||||
response += `Removed ${removeCategory.toString()} as a staff category\n`;
|
||||
} else {
|
||||
response += `Error removing staff category: ${removeCategory.toString()} isn't a staff category.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing staff category: ${removeCategory.toString()} isn't a category.`;
|
||||
}
|
||||
}
|
||||
|
||||
const addSafeChannel = interaction.options.getChannel('add-safe-channel');
|
||||
if (addSafeChannel) {
|
||||
if (addSafeChannel.type == ChannelType.GuildText) {
|
||||
await Database.putGuildArraySetting('safe_channels', interaction.guildId!, addSafeChannel.id);
|
||||
|
||||
response += `Added ${addSafeChannel.toString()} as a SFW cannel.\n`;
|
||||
} else {
|
||||
response += `Error adding SFW channel: ${addSafeChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeSafeChannel = interaction.options.getChannel('remove-safe-channel');
|
||||
if (removeSafeChannel) {
|
||||
if (removeSafeChannel.type == ChannelType.GuildText) {
|
||||
if (await Database.removeGuildArraySetting('safe_channels', interaction.guildId!, removeSafeChannel.id)) {
|
||||
response += `Removed ${removeSafeChannel.toString()} as a safe channel\n`;
|
||||
} else {
|
||||
response += `Error removing safe channel: ${removeSafeChannel.toString()} isn't a safe channel.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing safe channel: ${removeSafeChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const addLinkSkipChannel = interaction.options.getChannel('add-link-skip-channel');
|
||||
if (addLinkSkipChannel) {
|
||||
if (addLinkSkipChannel.type == ChannelType.GuildText) {
|
||||
await Database.putGuildArraySetting('link_skip_channels', interaction.guildId!, addLinkSkipChannel.id);
|
||||
|
||||
response += `Added ${addLinkSkipChannel.toString()} as a link skip channel.\n`;
|
||||
} else {
|
||||
response += `Error adding link skip channel: ${addLinkSkipChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeLinkSkipChannel = interaction.options.getChannel('remove-link-skip-channel');
|
||||
if (removeLinkSkipChannel) {
|
||||
if (removeLinkSkipChannel.type == ChannelType.GuildText) {
|
||||
if (await Database.removeGuildArraySetting('link_skip_channels', interaction.guildId!, removeLinkSkipChannel.id)) {
|
||||
response += `Removed ${removeLinkSkipChannel.toString()} as a staff category\n`;
|
||||
} else {
|
||||
response += `Error removing link skip channel: ${removeLinkSkipChannel.toString()} isn't a link skip channel.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing link skip channel: ${removeLinkSkipChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
import { ApplicationIntegrationType, ChannelType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('settings')
|
||||
.setDescription('Change server settings.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('general-channel')
|
||||
.setDescription('Set the general channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('tickets-channel')
|
||||
.setDescription('Set the ticket logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('event-logs-channel')
|
||||
.setDescription('Set the event logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('discord-logs-channel')
|
||||
.setDescription('Set the discord logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('audit-logs-channel')
|
||||
.setDescription('Set the audit logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('voice-logs-channel')
|
||||
.setDescription('Set the voice logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('new-member-channel')
|
||||
.setDescription('Set the new member logs channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('moderator-channel')
|
||||
.setDescription('Set the site moderator channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('admin-role')
|
||||
.setDescription('Set the admin role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('private-helper-role')
|
||||
.setDescription('Set the private helper role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(option =>
|
||||
option
|
||||
.setName('devwatch-role')
|
||||
.setDescription('Set the DevWatch role.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-staff-category')
|
||||
.setDescription('Add a category to staff categories.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('remove-staff-category')
|
||||
.setDescription('Remove a category from staff categories.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-safe-channel')
|
||||
.setDescription('Add a SFW channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('remove-safe-channel')
|
||||
.setDescription('Remove a SFW channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('add-link-skip-channel')
|
||||
.setDescription('Add a link skip channel.')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.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] });
|
||||
|
||||
let response = '';
|
||||
|
||||
const settings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!settings) {
|
||||
await Database.putGuild(interaction.guildId!);
|
||||
}
|
||||
|
||||
const generalChannel = interaction.options.getChannel('general-channel');
|
||||
|
||||
if (generalChannel) {
|
||||
await Database.setGuildGeneralChatId(interaction.guildId!, generalChannel.id);
|
||||
|
||||
response += `General channel set to ${generalChannel}.\n`;
|
||||
}
|
||||
|
||||
const ticketsChannel = interaction.options.getChannel('tickets-channel');
|
||||
|
||||
if (ticketsChannel) {
|
||||
await Database.setGuildTicketsLogsChannelId(interaction.guildId!, ticketsChannel.id);
|
||||
|
||||
response += `Tickets logs channel set to ${ticketsChannel}.\n`;
|
||||
}
|
||||
|
||||
const eventLogsChannel = interaction.options.getChannel('event-logs-channel');
|
||||
|
||||
if (eventLogsChannel) {
|
||||
await Database.setGuildEventsLogsChannelId(interaction.guildId!, eventLogsChannel.id);
|
||||
|
||||
response += `Event logs channel set to ${eventLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const discordLogsChannel = interaction.options.getChannel('discord-logs-channel');
|
||||
|
||||
if (discordLogsChannel) {
|
||||
await Database.setGuildDiscordLogsChannelId(interaction.guildId!, discordLogsChannel.id);
|
||||
|
||||
response += `Discord logs channel set to ${discordLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const auditLogsChannel = interaction.options.getChannel('audit-logs-channel');
|
||||
|
||||
if (auditLogsChannel) {
|
||||
await Database.setGuildAuditLogsChannelId(interaction.guildId!, auditLogsChannel.id);
|
||||
|
||||
response += `Audit logs channel set to ${auditLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const voiceLogsChannel = interaction.options.getChannel('voice-logs-channel');
|
||||
|
||||
if (voiceLogsChannel) {
|
||||
await Database.setGuildVoiceLogsChannelId(interaction.guildId!, voiceLogsChannel.id);
|
||||
|
||||
response += `Voice logs channel set to ${voiceLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const newMemberLogsChannel = interaction.options.getChannel('new-member-channel');
|
||||
|
||||
if (newMemberLogsChannel) {
|
||||
await Database.setGuildNewMemberLogsChannel(interaction.guildId!, newMemberLogsChannel.id);
|
||||
|
||||
response += `New member logs channel set to ${newMemberLogsChannel}.\n`;
|
||||
}
|
||||
|
||||
const moderatorChannel = interaction.options.getChannel('moderator-channel');
|
||||
|
||||
if (moderatorChannel) {
|
||||
await Database.setGuildModeratorChannel(interaction.guildId!, moderatorChannel.id);
|
||||
|
||||
response += `Moderator channel set to ${moderatorChannel}.\n`;
|
||||
}
|
||||
|
||||
const adminRole = interaction.options.getRole('admin-role');
|
||||
|
||||
if (adminRole) {
|
||||
await Database.setGuildAdminRole(interaction.guildId!, adminRole.id);
|
||||
|
||||
response += `Admin role set to ${adminRole}.\n`;
|
||||
}
|
||||
|
||||
const privateHelperRole = interaction.options.getRole('private-helper-role');
|
||||
|
||||
if (privateHelperRole) {
|
||||
await Database.setGuildPrivateHelperRole(interaction.guildId!, privateHelperRole.id);
|
||||
|
||||
response += `Private helper role set to ${privateHelperRole}.\n`;
|
||||
}
|
||||
|
||||
const devWatchRole = interaction.options.getRole('devwatch-role');
|
||||
|
||||
if (devWatchRole) {
|
||||
await Database.setGuildDevWatchRole(interaction.guildId!, devWatchRole.id);
|
||||
|
||||
response += `DevWatch role set to ${devWatchRole}.\n`;
|
||||
}
|
||||
|
||||
const addCategory = interaction.options.getChannel('add-staff-category');
|
||||
if (addCategory) {
|
||||
if (addCategory.type == ChannelType.GuildCategory) {
|
||||
await Database.putGuildArraySetting('staff_categories', interaction.guildId!, addCategory.id);
|
||||
|
||||
response += `Added ${addCategory.toString()} as a staff category.\n`;
|
||||
} else {
|
||||
response += `Error adding staff category: ${addCategory.toString()} isn't a category.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeCategory = interaction.options.getChannel('remove-staff-category');
|
||||
if (removeCategory) {
|
||||
if (removeCategory.type == ChannelType.GuildCategory) {
|
||||
if (await Database.removeGuildArraySetting('staff_categories', interaction.guildId!, removeCategory.id)) {
|
||||
response += `Removed ${removeCategory.toString()} as a staff category\n`;
|
||||
} else {
|
||||
response += `Error removing staff category: ${removeCategory.toString()} isn't a staff category.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing staff category: ${removeCategory.toString()} isn't a category.`;
|
||||
}
|
||||
}
|
||||
|
||||
const addSafeChannel = interaction.options.getChannel('add-safe-channel');
|
||||
if (addSafeChannel) {
|
||||
if (addSafeChannel.type == ChannelType.GuildText) {
|
||||
await Database.putGuildArraySetting('safe_channels', interaction.guildId!, addSafeChannel.id);
|
||||
|
||||
response += `Added ${addSafeChannel.toString()} as a SFW cannel.\n`;
|
||||
} else {
|
||||
response += `Error adding SFW channel: ${addSafeChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeSafeChannel = interaction.options.getChannel('remove-safe-channel');
|
||||
if (removeSafeChannel) {
|
||||
if (removeSafeChannel.type == ChannelType.GuildText) {
|
||||
if (await Database.removeGuildArraySetting('safe_channels', interaction.guildId!, removeSafeChannel.id)) {
|
||||
response += `Removed ${removeSafeChannel.toString()} as a safe channel\n`;
|
||||
} else {
|
||||
response += `Error removing safe channel: ${removeSafeChannel.toString()} isn't a safe channel.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing safe channel: ${removeSafeChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const addLinkSkipChannel = interaction.options.getChannel('add-link-skip-channel');
|
||||
if (addLinkSkipChannel) {
|
||||
if (addLinkSkipChannel.type == ChannelType.GuildText) {
|
||||
await Database.putGuildArraySetting('link_skip_channels', interaction.guildId!, addLinkSkipChannel.id);
|
||||
|
||||
response += `Added ${addLinkSkipChannel.toString()} as a link skip channel.\n`;
|
||||
} else {
|
||||
response += `Error adding link skip channel: ${addLinkSkipChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
const removeLinkSkipChannel = interaction.options.getChannel('remove-link-skip-channel');
|
||||
if (removeLinkSkipChannel) {
|
||||
if (removeLinkSkipChannel.type == ChannelType.GuildText) {
|
||||
if (await Database.removeGuildArraySetting('link_skip_channels', interaction.guildId!, removeLinkSkipChannel.id)) {
|
||||
response += `Removed ${removeLinkSkipChannel.toString()} as a staff category\n`;
|
||||
} else {
|
||||
response += `Error removing link skip channel: ${removeLinkSkipChannel.toString()} isn't a link skip channel.`;
|
||||
}
|
||||
} else {
|
||||
response += `Error removing link skip channel: ${removeLinkSkipChannel.toString()} isn't a text channel.`;
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
};
|
||||
+80
-80
@@ -1,81 +1,81 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { deferInteraction } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'softban',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('softban')
|
||||
.setDescription('Bans and immediately unbans a user to purge messages.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user to softban.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the softban')
|
||||
.setRequired(false)
|
||||
.setMaxLength(400)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('days')
|
||||
.setDescription('How far back to delete messages (in days, default: 7 days).')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(7)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
|
||||
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const reason = interaction.options.getString('reason') ?? '';
|
||||
const seconds = (interaction.options.getNumber('days') ?? 7) * 86400;
|
||||
|
||||
let banMember: GuildMember | null = null;
|
||||
|
||||
try {
|
||||
banMember = await interaction.guild.members.fetch(user.id);
|
||||
} catch (e) {
|
||||
// Member not in server.
|
||||
}
|
||||
|
||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
|
||||
return await interaction.editReply('You do not have permission to softban this user.');
|
||||
}
|
||||
|
||||
if (banMember && !banMember.bannable) {
|
||||
return await interaction.editReply('I do not have permission to softban this user.');
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.create(user, {
|
||||
reason: (reason + ` Softban by ${interaction.user.username} (${interaction.user.id})`).trim(),
|
||||
deleteMessageSeconds: seconds
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error softbanning user (couldn't ban).");
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.remove(user);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error softbanning user (couldn't remove ban).");
|
||||
}
|
||||
|
||||
await interaction.editReply('Softban successful');
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { deferInteraction } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'softban',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('softban')
|
||||
.setDescription('Bans and immediately unbans a user to purge messages.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user to softban.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the softban')
|
||||
.setRequired(false)
|
||||
.setMaxLength(400)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('days')
|
||||
.setDescription('How far back to delete messages (in days, default: 7 days).')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(7)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
|
||||
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const reason = interaction.options.getString('reason') ?? '';
|
||||
const seconds = (interaction.options.getNumber('days') ?? 7) * 86400;
|
||||
|
||||
let banMember: GuildMember | null = null;
|
||||
|
||||
try {
|
||||
banMember = await interaction.guild.members.fetch(user.id);
|
||||
} catch (e) {
|
||||
// Member not in server.
|
||||
}
|
||||
|
||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
|
||||
return await interaction.editReply('You do not have permission to softban this user.');
|
||||
}
|
||||
|
||||
if (banMember && !banMember.bannable) {
|
||||
return await interaction.editReply('I do not have permission to softban this user.');
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.create(user, {
|
||||
reason: (reason + ` Softban by ${interaction.user.username} (${interaction.user.id})`).trim(),
|
||||
deleteMessageSeconds: seconds
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error softbanning user (couldn't ban).");
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.guild.bans.remove(user);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return await interaction.editReply("Error softbanning user (couldn't remove ban).");
|
||||
}
|
||||
|
||||
await interaction.editReply('Softban successful');
|
||||
}
|
||||
};
|
||||
+29
-29
@@ -1,30 +1,30 @@
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'whois',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('whois')
|
||||
.setDescription("Find a user's e621 account from their discord account, or vice versa.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to find the e621 user of.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const valueToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
handleWhoIsInteraction(interaction, valueToUse, !(await channelIsInStaffCategory(interaction.channel as GuildBasedChannel)));
|
||||
}
|
||||
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageMentions, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||
|
||||
const mentionRegex = new RegExp(MessageMentions.UsersPattern);
|
||||
|
||||
export default {
|
||||
name: 'whois',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('whois')
|
||||
.setDescription("Find a user's e621 account from their discord account, or vice versa.")
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The discord user mention, or ID, to find the e621 user of.')
|
||||
.setRequired(true)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const input = interaction.options.getString('user', true);
|
||||
|
||||
const matches = mentionRegex.exec(input);
|
||||
mentionRegex.lastIndex = 0;
|
||||
|
||||
const valueToUse = matches ? matches.groups!.id : input;
|
||||
|
||||
handleWhoIsInteraction(interaction, valueToUse, !(await channelIsInStaffCategory(interaction.channel as GuildBasedChannel)));
|
||||
}
|
||||
};
|
||||
+26
-26
@@ -1,27 +1,27 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, GIT_REPO_BASE_URL, REDIS_URL, PORT, DEBUG } = 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,
|
||||
GIT_REPO_BASE_URL,
|
||||
PORT: parseInt(PORT as string),
|
||||
REDIS_URL,
|
||||
DEV_MODE: process.env.npm_lifecycle_event == 'dev',
|
||||
DEBUG: DEBUG == 'true'
|
||||
};
|
||||
|
||||
for (const [key, val] of Object.entries(config)) {
|
||||
if (val === undefined) {
|
||||
throw new Error(`${key} is undefined in config`);
|
||||
}
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, GIT_REPO_BASE_URL, REDIS_URL, PORT, DEBUG } = 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,
|
||||
GIT_REPO_BASE_URL,
|
||||
PORT: parseInt(PORT as string),
|
||||
REDIS_URL,
|
||||
DEV_MODE: process.env.npm_lifecycle_event == 'dev',
|
||||
DEBUG: DEBUG == 'true'
|
||||
};
|
||||
|
||||
for (const [key, val] of Object.entries(config)) {
|
||||
if (val === undefined) {
|
||||
throw new Error(`${key} is undefined in config`);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, ModalBuilder, PermissionFlagsBits, TextInputStyle, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { createTextInput } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Add Note',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Add Note')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const member = await interaction.guild?.members.fetch(idToUse);
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`add-note-modal_${idToUse}`)
|
||||
.setTitle(`Adding note to ${member ? member.displayName : idToUse}`);
|
||||
|
||||
const inputLabel = createTextInput('note-message', 'Note Message', null, true, TextInputStyle.Paragraph, 1500, 2);
|
||||
|
||||
modal.addLabelComponents(inputLabel);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, ModalBuilder, PermissionFlagsBits, TextInputStyle, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { createTextInput } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Add Note',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Add Note')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const member = await interaction.guild?.members.fetch(idToUse);
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`add-note-modal_${idToUse}`)
|
||||
.setTitle(`Adding note to ${member ? member.displayName : idToUse}`);
|
||||
|
||||
const inputLabel = createTextInput('note-message', 'Note Message', null, true, TextInputStyle.Paragraph, 1500, 2);
|
||||
|
||||
modal.addLabelComponents(inputLabel);
|
||||
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, PermissionFlagsBits, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { deferInteraction, getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'List Notes',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('List Notes')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const noteMessage = await getNoteMessage(idToUse, 1);
|
||||
|
||||
if (!noteMessage) return interaction.editReply(`No notes found for <@${idToUse}>`);
|
||||
|
||||
interaction.editReply(noteMessage);
|
||||
}
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, PermissionFlagsBits, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { deferInteraction, getNoteMessage } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'List Notes',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('List Notes')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const noteMessage = await getNoteMessage(idToUse, 1);
|
||||
|
||||
if (!noteMessage) return interaction.editReply(`No notes found for <@${idToUse}>`);
|
||||
|
||||
interaction.editReply(noteMessage);
|
||||
}
|
||||
};
|
||||
@@ -1,19 +1,19 @@
|
||||
import { ApplicationIntegrationType, Client, InteractionContextType, PermissionFlagsBits, ContextMenuCommandBuilder, ApplicationCommandType, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { openModTicketModal } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Open Mod Ticket',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Open Mod Ticket')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const member = await interaction.guild?.members.fetch(interaction.targetUser.id);
|
||||
|
||||
if (!member) return interaction.editReply('Could not find member.');
|
||||
|
||||
openModTicketModal(interaction, member);
|
||||
}
|
||||
import { ApplicationIntegrationType, Client, InteractionContextType, PermissionFlagsBits, ContextMenuCommandBuilder, ApplicationCommandType, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { openModTicketModal } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Open Mod Ticket',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Open Mod Ticket')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const member = await interaction.guild?.members.fetch(interaction.targetUser.id);
|
||||
|
||||
if (!member) return interaction.editReply('Could not find member.');
|
||||
|
||||
openModTicketModal(interaction, member);
|
||||
}
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, PermissionFlagsBits, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { deferInteraction, getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Get Records',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Get Records')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const recordMessage = await getRecordMessageFromDiscordId(idToUse, 1, interaction.guild!);
|
||||
|
||||
if (!recordMessage) return interaction.editReply('No records found on any linked accounts.');
|
||||
|
||||
interaction.editReply(recordMessage);
|
||||
}
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, PermissionFlagsBits, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { deferInteraction, getRecordMessageFromDiscordId } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Get Records',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Get Records')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const recordMessage = await getRecordMessageFromDiscordId(idToUse, 1, interaction.guild!);
|
||||
|
||||
if (!recordMessage) return interaction.editReply('No records found on any linked accounts.');
|
||||
|
||||
interaction.editReply(recordMessage);
|
||||
}
|
||||
};
|
||||
@@ -1,57 +1,57 @@
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, ModalBuilder, PermissionFlagsBits, TextInputStyle, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createTextInput, deferInteraction, syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Sync Name',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Sync Name')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageNicknames)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const availableIds = await Database.getE621Ids(interaction.user.id);
|
||||
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const member = await guild.members.fetch(idToUse);
|
||||
|
||||
const interactionMember = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member || !interactionMember) {
|
||||
return interaction.reply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
if (interactionMember.roles.highest.comparePositionTo(member.roles.highest) <= 0) {
|
||||
return await interaction.editReply("You do not have permission to sync this user's name.");
|
||||
}
|
||||
|
||||
if (availableIds.length > 1) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`sync-name-modal_${idToUse}`)
|
||||
.setTitle(`Syncing ${member ? member.displayName : idToUse}'s name`);
|
||||
|
||||
const inputLabel = createTextInput('id', 'User has multiple linked accounts. Provide ID', null, false, TextInputStyle.Short, null, null);
|
||||
|
||||
modal.addLabelComponents(inputLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, null);
|
||||
}
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, ModalBuilder, PermissionFlagsBits, TextInputStyle, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createTextInput, deferInteraction, syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Sync Name',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Sync Name')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageNicknames)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const availableIds = await Database.getE621Ids(interaction.user.id);
|
||||
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const member = await guild.members.fetch(idToUse);
|
||||
|
||||
const interactionMember = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
if (!member || !interactionMember) {
|
||||
return interaction.reply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
if (interactionMember.roles.highest.comparePositionTo(member.roles.highest) <= 0) {
|
||||
return await interaction.editReply("You do not have permission to sync this user's name.");
|
||||
}
|
||||
|
||||
if (availableIds.length > 1) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`sync-name-modal_${idToUse}`)
|
||||
.setTitle(`Syncing ${member ? member.displayName : idToUse}'s name`);
|
||||
|
||||
const inputLabel = createTextInput('id', 'User has multiple linked accounts. Provide ID', null, false, TextInputStyle.Short, null, null);
|
||||
|
||||
modal.addLabelComponents(inputLabel);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
if (!guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, null);
|
||||
}
|
||||
};
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
import { ApplicationIntegrationType, Client, InteractionContextType, PermissionFlagsBits, ContextMenuCommandBuilder, ApplicationCommandType, UserContextMenuCommandInteraction, GuildBasedChannel } from 'discord.js';
|
||||
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Whois',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Whois')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
handleWhoIsInteraction(interaction, idToUse, !(await channelIsInStaffCategory(interaction.channel as GuildBasedChannel)));
|
||||
}
|
||||
import { ApplicationIntegrationType, Client, InteractionContextType, PermissionFlagsBits, ContextMenuCommandBuilder, ApplicationCommandType, UserContextMenuCommandInteraction, GuildBasedChannel } from 'discord.js';
|
||||
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'Whois',
|
||||
data: new ContextMenuCommandBuilder()
|
||||
.setName('Whois')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.setType(ApplicationCommandType.User),
|
||||
handler: async function (client: Client, interaction: UserContextMenuCommandInteraction) {
|
||||
const idToUse = interaction.targetUser.id;
|
||||
|
||||
handleWhoIsInteraction(interaction, idToUse, !(await channelIsInStaffCategory(interaction.channel as GuildBasedChannel)));
|
||||
}
|
||||
};
|
||||
@@ -1,94 +1,94 @@
|
||||
import { APIEmbedField, APIRole, AuditLogEvent, EmbedBuilder, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { formatChanges, formatExtras, formatSnowflake, getTargetType } from '../utils';
|
||||
|
||||
const IGNORED_ACTIONS = [
|
||||
AuditLogEvent.MemberMove,
|
||||
// Handled by automod.
|
||||
AuditLogEvent.AutoModerationFlagToChannel
|
||||
];
|
||||
|
||||
export async function handleAuditLogCreate(entry: GuildAuditLogsEntry, guild: Guild) {
|
||||
if (!await shouldLog(entry, guild)) return;
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings || !settings.audit_logs_channel_id) return;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.audit_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const fields: APIEmbedField[] = [
|
||||
{
|
||||
name: 'Actor',
|
||||
value: `<@${entry.executorId}>`,
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
|
||||
if (entry.targetId) {
|
||||
const targetType = getTargetType(entry.action);
|
||||
fields.push({
|
||||
name: 'Target',
|
||||
value: formatSnowflake(entry.targetId, targetType),
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.reason) {
|
||||
fields.push({
|
||||
name: 'Reason',
|
||||
value: entry.reason,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.changes && entry.changes.length > 0) {
|
||||
fields.push({
|
||||
name: 'Changes',
|
||||
value: formatChanges(entry),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.extra) {
|
||||
fields.push({
|
||||
name: 'Options',
|
||||
value: formatExtras(entry, guild),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(Object.keys(AuditLogEvent)[Object.values(AuditLogEvent).indexOf(entry.action)])
|
||||
.setTimestamp(Number(SnowflakeUtil.decode(entry.id).timestamp))
|
||||
.addFields(...fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function shouldLog(entry: GuildAuditLogsEntry, guild: Guild): Promise<boolean> {
|
||||
if (!entry.executorId) return true;
|
||||
|
||||
if (IGNORED_ACTIONS.some(a => entry.action == a)) return false;
|
||||
|
||||
if (entry.action == AuditLogEvent.MemberRoleUpdate) {
|
||||
return await shouldLogRoleChanges(entry as GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild: Guild): Promise<boolean> {
|
||||
for (const change of entry.changes) {
|
||||
// Get role changes from the log.
|
||||
const roles = (await Promise.all((change.new! as Pick<APIRole, 'id' | 'name'>[]).map(c => guild.roles.fetch(c.id))));
|
||||
|
||||
// Check if role is part of onboarding.
|
||||
for (const role of roles) {
|
||||
if (role && !role.flags.has(RoleFlags.InPrompt)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
import { APIEmbedField, APIRole, AuditLogEvent, EmbedBuilder, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { formatChanges, formatExtras, formatSnowflake, getTargetType } from '../utils';
|
||||
|
||||
const IGNORED_ACTIONS = [
|
||||
AuditLogEvent.MemberMove,
|
||||
// Handled by automod.
|
||||
AuditLogEvent.AutoModerationFlagToChannel
|
||||
];
|
||||
|
||||
export async function handleAuditLogCreate(entry: GuildAuditLogsEntry, guild: Guild) {
|
||||
if (!await shouldLog(entry, guild)) return;
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings || !settings.audit_logs_channel_id) return;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.audit_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const fields: APIEmbedField[] = [
|
||||
{
|
||||
name: 'Actor',
|
||||
value: `<@${entry.executorId}>`,
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
|
||||
if (entry.targetId) {
|
||||
const targetType = getTargetType(entry.action);
|
||||
fields.push({
|
||||
name: 'Target',
|
||||
value: formatSnowflake(entry.targetId, targetType),
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.reason) {
|
||||
fields.push({
|
||||
name: 'Reason',
|
||||
value: entry.reason,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.changes && entry.changes.length > 0) {
|
||||
fields.push({
|
||||
name: 'Changes',
|
||||
value: formatChanges(entry),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.extra) {
|
||||
fields.push({
|
||||
name: 'Options',
|
||||
value: formatExtras(entry, guild),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(Object.keys(AuditLogEvent)[Object.values(AuditLogEvent).indexOf(entry.action)])
|
||||
.setTimestamp(Number(SnowflakeUtil.decode(entry.id).timestamp))
|
||||
.addFields(...fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function shouldLog(entry: GuildAuditLogsEntry, guild: Guild): Promise<boolean> {
|
||||
if (!entry.executorId) return true;
|
||||
|
||||
if (IGNORED_ACTIONS.some(a => entry.action == a)) return false;
|
||||
|
||||
if (entry.action == AuditLogEvent.MemberRoleUpdate) {
|
||||
return await shouldLogRoleChanges(entry as GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild: Guild): Promise<boolean> {
|
||||
for (const change of entry.changes) {
|
||||
// Get role changes from the log.
|
||||
const roles = (await Promise.all((change.new! as Pick<APIRole, 'id' | 'name'>[]).map(c => guild.roles.fetch(c.id))));
|
||||
|
||||
// Check if role is part of onboarding.
|
||||
for (const role of roles) {
|
||||
if (role && !role.flags.has(RoleFlags.InPrompt)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GuildBan } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleBanRemove(ban: GuildBan) {
|
||||
await Database.removeBan(ban.user.id);
|
||||
import { GuildBan } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleBanRemove(ban: GuildBan) {
|
||||
await Database.removeBan(ban.user.id);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Guild } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleGuildCreate(guild: Guild) {
|
||||
try {
|
||||
if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
import { Guild } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleGuildCreate(guild: Guild) {
|
||||
try {
|
||||
if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
import { GuildMember, GuildTextBasedChannel } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { getE621Alts } from '../utils';
|
||||
|
||||
export async function handleMemberJoin(member: GuildMember) {
|
||||
const guildSettings = await Database.getGuildSettings(member.guild.id);
|
||||
|
||||
if (guildSettings?.new_member_channel_id) {
|
||||
const channel = await member.guild.channels.fetch(guildSettings.new_member_channel_id) as GuildTextBasedChannel;
|
||||
|
||||
if (channel) {
|
||||
const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`;
|
||||
|
||||
channel.send(content).catch(console.error);
|
||||
|
||||
if (guildSettings.moderator_channel_id && content.includes('[BANNED]')) {
|
||||
const modChannel = await member.guild.channels.fetch(guildSettings.moderator_channel_id) as GuildTextBasedChannel;
|
||||
|
||||
if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { GuildMember, GuildTextBasedChannel } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { getE621Alts } from '../utils';
|
||||
|
||||
export async function handleMemberJoin(member: GuildMember) {
|
||||
const guildSettings = await Database.getGuildSettings(member.guild.id);
|
||||
|
||||
if (guildSettings?.new_member_channel_id) {
|
||||
const channel = await member.guild.channels.fetch(guildSettings.new_member_channel_id) as GuildTextBasedChannel;
|
||||
|
||||
if (channel) {
|
||||
const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`;
|
||||
|
||||
channel.send(content).catch(console.error);
|
||||
|
||||
if (guildSettings.moderator_channel_id && content.includes('[BANNED]')) {
|
||||
const modChannel = await member.guild.channels.fetch(guildSettings.moderator_channel_id) as GuildTextBasedChannel;
|
||||
|
||||
if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+384
-384
@@ -1,385 +1,385 @@
|
||||
import { Message as DiscordMessage, GuildBasedChannel, GuildTextBasedChannel, OmitPartialGroupDMChannel, PartialMessage, ReadonlyCollection, spoiler } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { E621Post } from '../types';
|
||||
import { ALLOWED_MIMETYPES, artistIDRegex, blipIDRegex, calculateMD5FromURL, channelIgnoresLinks, channelIsInStaffCategory, channelIsSafe, commentIDRegex, forumTopicIDRegex, getE621Post, getE621PostByMd5, getPostUrl, isEdited, isInSpoilerTags, issueRegex, logDeletion, logEdit, poolIDRegex, PostAction, postIDRegex, prRegex, recordIDRegex, searchLinkRegex, setIDRegex, spoilerOrBlacklist, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from '../utils';
|
||||
|
||||
export type Message<InGuild extends boolean = boolean> = OmitPartialGroupDMChannel<DiscordMessage<InGuild>>;
|
||||
export type Partial = OmitPartialGroupDMChannel<PartialMessage>;
|
||||
|
||||
// TODO: I don't know of any good way to not hardcode this regex for e621 links. So I've provided two that may need to have the port altered.
|
||||
const postRegex = new RegExp('!?https?://(?:.*@)?(?:e621|e926)\\.net/+posts/+([0-9]+)', 'gi');
|
||||
const postShareRegex = new RegExp('!?https?://(?:.*@)?(?:e621|e926)\\.net/+p/+([a-z0-9]+)', 'gi');
|
||||
const imageRegex = new RegExp('!?https?://(?:.*@)?static[0-9]*\\.(?:e621|e926)\\.net/+data/+(?:sample/+|preview/+|)[\\da-f]{2}/+[\\da-f]{2}/+([\\da-f]{32})\\.[\\da-z]+', 'gi');
|
||||
|
||||
const postRegex_DEV = new RegExp('!?https?://(?:.*@)?localhost:3000/+posts/+([0-9]+)', 'gi');
|
||||
const imageRegex_DEV = new RegExp('!?https?://(?:.*@)?localhost:3000/+data/+(?:sample/+|preview/+|)[\\da-f]{2}/+[\\da-f]{2}/+([\\da-f]{32})\\.[\\da-z]+', 'gi');
|
||||
|
||||
const md5Regex = new RegExp('^([a-f0-9]{32}).(?:png|apng|jpg|jpeg|gif|webm|mp4)$', 'gi');
|
||||
|
||||
const regexTesters = [
|
||||
{ runInDev: false, regex: postRegex, handler: postHandler.bind(null, null) },
|
||||
{
|
||||
runInDev: false, regex: postShareRegex, handler: postHandler.bind(null, (idString: string) => {
|
||||
return parseInt(idString, 32);
|
||||
})
|
||||
},
|
||||
{ runInDev: false, regex: imageRegex, handler: imageHandler },
|
||||
{ runInDev: true, regex: postRegex_DEV, handler: postHandler.bind(null, null) },
|
||||
{ runInDev: true, regex: imageRegex_DEV, handler: imageHandler },
|
||||
{ runInDev: true, regex: postIDRegex, handler: postIdHandler },
|
||||
{ runInDev: true, regex: userIDRegex, handler: idHandler.bind(null, 'users') },
|
||||
{ runInDev: true, regex: forumTopicIDRegex, handler: idHandler.bind(null, 'forum_topics') },
|
||||
{ runInDev: true, regex: commentIDRegex, handler: idHandler.bind(null, 'comments') },
|
||||
{ runInDev: true, regex: blipIDRegex, handler: idHandler.bind(null, 'blips') },
|
||||
{ runInDev: true, regex: poolIDRegex, handler: idHandler.bind(null, 'pools') },
|
||||
{ runInDev: true, regex: setIDRegex, handler: idHandler.bind(null, 'post_sets') },
|
||||
{ runInDev: true, regex: takedownIDRegex, handler: idHandler.bind(null, 'takedowns') },
|
||||
{ runInDev: true, regex: recordIDRegex, handler: idHandler.bind(null, 'user_feedbacks') },
|
||||
{ runInDev: true, regex: ticketIDRegex, handler: idHandler.bind(null, 'tickets') },
|
||||
{ runInDev: true, regex: artistIDRegex, handler: idHandler.bind(null, 'artists') },
|
||||
{ runInDev: true, regex: wikiLinkRegex, handler: wikiPageHandler },
|
||||
{ runInDev: true, regex: searchLinkRegex, handler: searchHandler },
|
||||
|
||||
{ runInDev: true, regex: prRegex, handler: githubPullRequestHandler },
|
||||
{ runInDev: true, regex: issueRegex, handler: githubIssueHandler },
|
||||
];
|
||||
|
||||
const uniqueRegexMatches = (g, i, a) => a.findIndex(v => v[1] == g[1]) == i;
|
||||
|
||||
export async function handleMessageCreate(message: Message) {
|
||||
if (message.author.bot) return;
|
||||
if (message.inGuild()) await Database.putMessage(message);
|
||||
|
||||
const responses: string[] = [];
|
||||
|
||||
for (const test of regexTesters) {
|
||||
if (config.DEV_MODE && !test.runInDev) continue;
|
||||
|
||||
const hasMatches = test.regex.test(message.content);
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
if (hasMatches) {
|
||||
const matches: RegExpExecArray[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = test.regex.exec(message.content)) != null) {
|
||||
matches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
const response = await test.handler(message, matches.filter(uniqueRegexMatches));
|
||||
|
||||
if (response === false) return;
|
||||
|
||||
if (response !== true) responses.push(response as string);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attachment of message.attachments.values()) {
|
||||
const match = md5Regex.exec(attachment.name);
|
||||
md5Regex.lastIndex = 0;
|
||||
|
||||
const md5s: string[] = [];
|
||||
|
||||
if (match) md5s.push(match[1]);
|
||||
else if (ALLOWED_MIMETYPES.includes(attachment.contentType!)) {
|
||||
const md5Data = await calculateMD5FromURL(attachment.url);
|
||||
if (!md5Data) continue;
|
||||
md5s.push(md5Data.correctedFileMD5, md5Data.originalFileMD5);
|
||||
}
|
||||
|
||||
if (md5s.length == 0) continue;
|
||||
|
||||
for (const md5 of md5s) {
|
||||
const post = await getE621PostByMd5(md5);
|
||||
|
||||
if (post) {
|
||||
if (await blacklistIfNecessary(message, [post])) return;
|
||||
|
||||
responses.push(`<${getPostUrl(post)}>`);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
await message.reply(responses.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMessageUpdate(oldMessage: Message | PartialMessage, newMessage: Message) {
|
||||
if (newMessage.author.bot) return;
|
||||
|
||||
const loggedMessage = await Database.getMessageWithRetry(newMessage.id);
|
||||
|
||||
if (!loggedMessage) {
|
||||
if (newMessage.inGuild()) await Database.putMessage(newMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMessage.inGuild() && isEdited(loggedMessage, newMessage)) {
|
||||
await Database.putMessage(newMessage);
|
||||
await logEdit(loggedMessage, newMessage);
|
||||
}
|
||||
|
||||
if (loggedMessage.content == newMessage.content) return;
|
||||
|
||||
const responses: string[] = [];
|
||||
|
||||
for (const test of regexTesters) {
|
||||
if (config.DEV_MODE && !test.runInDev) continue;
|
||||
|
||||
const hasMatches = test.regex.test(newMessage.content);
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
if (hasMatches) {
|
||||
const oldMatches: RegExpExecArray[] = [];
|
||||
const newMatches: RegExpExecArray[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = test.regex.exec(newMessage.content)) != null) {
|
||||
newMatches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
while ((match = test.regex.exec(loggedMessage.content)) != null) {
|
||||
oldMatches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
const properMatches: RegExpExecArray[] = [];
|
||||
|
||||
for (const newMatch of newMatches) {
|
||||
if (!oldMatches.find(m => m[1] == newMatch[1])) properMatches.push(newMatch);
|
||||
}
|
||||
|
||||
if (properMatches.length == 0) continue;
|
||||
|
||||
const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches));
|
||||
|
||||
if (response === false) return;
|
||||
|
||||
if (response !== true) responses.push(response as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
await newMessage.reply(responses.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMessageDelete(message: Message | PartialMessage) {
|
||||
const loggedMessage = await Database.getMessageWithRetry(message.id);
|
||||
|
||||
if (!loggedMessage) return;
|
||||
|
||||
if (message.inGuild()) await logDeletion(loggedMessage, message);
|
||||
}
|
||||
|
||||
export async function handleBulkMessageDelete(messages: ReadonlyCollection<string, Message | Partial>, channel: GuildTextBasedChannel) {
|
||||
for (const message of messages.values()) {
|
||||
await handleMessageDelete(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function searchHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `<${config.E621_BASE_URL}/posts?tags=${encodeURIComponent(group[1])}>\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function wikiPageHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `<${config.E621_BASE_URL}/wiki_pages/${group[1].split('#').map(t => encodeURIComponent(t)).join('#')}>\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function blacklistIfNecessary(message: Message, posts: E621Post[]): Promise<boolean> {
|
||||
const blacklistedIds: number[] = [];
|
||||
|
||||
const channel = await message.channel.fetch() as GuildTextBasedChannel;
|
||||
|
||||
const isStaffChannel = await channelIsInStaffCategory(channel);
|
||||
|
||||
for (const post of posts) {
|
||||
if (spoilerOrBlacklist(post).action == PostAction.Blacklist) {
|
||||
blacklistedIds.push(post.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (blacklistedIds.length == 0) return false;
|
||||
|
||||
await message.delete();
|
||||
|
||||
if (channel.parentId && isStaffChannel) {
|
||||
await message.channel.send({
|
||||
content: `_sucks message into the void._ ${message.author.toString()} nono, don't post links to ${blacklistedIds.length == 1 ? `post ${blacklistedIds[0]}` : `posts \`${blacklistedIds.join('`, `')}\``}. See rule #5.b for more details.`,
|
||||
allowedMentions: {
|
||||
users: [message.author.id]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await message.channel.send({
|
||||
content: `_sucks message into the void._ ${message.author.toString()} nono, don't post links to young/cub content. See rule #5.b for more details.`,
|
||||
allowedMentions: {
|
||||
users: [message.author.id]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function postIdHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: { post: E621Post, spoilered: boolean }[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621Post(match[1]);
|
||||
if (post) posts.push({
|
||||
spoilered: isInSpoilerTags(message.content, match.index),
|
||||
post
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts.map(p => p.post))) return false;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const sfw = await channelIsSafe(message.channel as GuildBasedChannel);
|
||||
|
||||
const content = posts.map((postData) => {
|
||||
if (sfw && postData.post.rating != 's') return ` [NSFW] <${getPostUrl(postData.post)}>`;
|
||||
|
||||
const shouldSpoiler = spoilerOrBlacklist(postData.post);
|
||||
if (shouldSpoiler.action == PostAction.Spoiler) return `${spoiler(getPostUrl(postData.post))} (${shouldSpoiler.tag})`;
|
||||
|
||||
return postData.spoilered ? spoiler(getPostUrl(postData.post)) : getPostUrl(postData.post);
|
||||
}).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function idHandler(path: string, message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const content = matchedGroups.map(m => `${config.E621_BASE_URL}/${path}/${m[1]}`).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function postHandler(transform: ((idString: string) => number) | null, message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: E621Post[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621Post(transform ? transform(match[1]) : match[1]);
|
||||
if (post) posts.push(post);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function imageHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: E621Post[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621PostByMd5(match[1]);
|
||||
if (post) posts.push(post);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts)) return false;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const content = posts.map(post => `<${getPostUrl(post)}>`).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function githubPullRequestHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `${config.GIT_REPO_BASE_URL}/pull/${group[1]}\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function githubIssueHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `${config.GIT_REPO_BASE_URL}/issues/${group[1]}\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
import { Message as DiscordMessage, GuildBasedChannel, GuildTextBasedChannel, OmitPartialGroupDMChannel, PartialMessage, ReadonlyCollection, spoiler } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { E621Post } from '../types';
|
||||
import { ALLOWED_MIMETYPES, artistIDRegex, blipIDRegex, calculateMD5FromURL, channelIgnoresLinks, channelIsInStaffCategory, channelIsSafe, commentIDRegex, forumTopicIDRegex, getE621Post, getE621PostByMd5, getPostUrl, isEdited, isInSpoilerTags, issueRegex, logDeletion, logEdit, poolIDRegex, PostAction, postIDRegex, prRegex, recordIDRegex, searchLinkRegex, setIDRegex, spoilerOrBlacklist, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from '../utils';
|
||||
|
||||
export type Message<InGuild extends boolean = boolean> = OmitPartialGroupDMChannel<DiscordMessage<InGuild>>;
|
||||
export type Partial = OmitPartialGroupDMChannel<PartialMessage>;
|
||||
|
||||
// TODO: I don't know of any good way to not hardcode this regex for e621 links. So I've provided two that may need to have the port altered.
|
||||
const postRegex = new RegExp('!?https?://(?:.*@)?(?:e621|e926)\\.net/+posts/+([0-9]+)', 'gi');
|
||||
const postShareRegex = new RegExp('!?https?://(?:.*@)?(?:e621|e926)\\.net/+p/+([a-z0-9]+)', 'gi');
|
||||
const imageRegex = new RegExp('!?https?://(?:.*@)?static[0-9]*\\.(?:e621|e926)\\.net/+data/+(?:sample/+|preview/+|)[\\da-f]{2}/+[\\da-f]{2}/+([\\da-f]{32})\\.[\\da-z]+', 'gi');
|
||||
|
||||
const postRegex_DEV = new RegExp('!?https?://(?:.*@)?localhost:3000/+posts/+([0-9]+)', 'gi');
|
||||
const imageRegex_DEV = new RegExp('!?https?://(?:.*@)?localhost:3000/+data/+(?:sample/+|preview/+|)[\\da-f]{2}/+[\\da-f]{2}/+([\\da-f]{32})\\.[\\da-z]+', 'gi');
|
||||
|
||||
const md5Regex = new RegExp('^([a-f0-9]{32}).(?:png|apng|jpg|jpeg|gif|webm|mp4)$', 'gi');
|
||||
|
||||
const regexTesters = [
|
||||
{ runInDev: false, regex: postRegex, handler: postHandler.bind(null, null) },
|
||||
{
|
||||
runInDev: false, regex: postShareRegex, handler: postHandler.bind(null, (idString: string) => {
|
||||
return parseInt(idString, 32);
|
||||
})
|
||||
},
|
||||
{ runInDev: false, regex: imageRegex, handler: imageHandler },
|
||||
{ runInDev: true, regex: postRegex_DEV, handler: postHandler.bind(null, null) },
|
||||
{ runInDev: true, regex: imageRegex_DEV, handler: imageHandler },
|
||||
{ runInDev: true, regex: postIDRegex, handler: postIdHandler },
|
||||
{ runInDev: true, regex: userIDRegex, handler: idHandler.bind(null, 'users') },
|
||||
{ runInDev: true, regex: forumTopicIDRegex, handler: idHandler.bind(null, 'forum_topics') },
|
||||
{ runInDev: true, regex: commentIDRegex, handler: idHandler.bind(null, 'comments') },
|
||||
{ runInDev: true, regex: blipIDRegex, handler: idHandler.bind(null, 'blips') },
|
||||
{ runInDev: true, regex: poolIDRegex, handler: idHandler.bind(null, 'pools') },
|
||||
{ runInDev: true, regex: setIDRegex, handler: idHandler.bind(null, 'post_sets') },
|
||||
{ runInDev: true, regex: takedownIDRegex, handler: idHandler.bind(null, 'takedowns') },
|
||||
{ runInDev: true, regex: recordIDRegex, handler: idHandler.bind(null, 'user_feedbacks') },
|
||||
{ runInDev: true, regex: ticketIDRegex, handler: idHandler.bind(null, 'tickets') },
|
||||
{ runInDev: true, regex: artistIDRegex, handler: idHandler.bind(null, 'artists') },
|
||||
{ runInDev: true, regex: wikiLinkRegex, handler: wikiPageHandler },
|
||||
{ runInDev: true, regex: searchLinkRegex, handler: searchHandler },
|
||||
|
||||
{ runInDev: true, regex: prRegex, handler: githubPullRequestHandler },
|
||||
{ runInDev: true, regex: issueRegex, handler: githubIssueHandler },
|
||||
];
|
||||
|
||||
const uniqueRegexMatches = (g, i, a) => a.findIndex(v => v[1] == g[1]) == i;
|
||||
|
||||
export async function handleMessageCreate(message: Message) {
|
||||
if (message.author.bot) return;
|
||||
if (message.inGuild()) await Database.putMessage(message);
|
||||
|
||||
const responses: string[] = [];
|
||||
|
||||
for (const test of regexTesters) {
|
||||
if (config.DEV_MODE && !test.runInDev) continue;
|
||||
|
||||
const hasMatches = test.regex.test(message.content);
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
if (hasMatches) {
|
||||
const matches: RegExpExecArray[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = test.regex.exec(message.content)) != null) {
|
||||
matches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
const response = await test.handler(message, matches.filter(uniqueRegexMatches));
|
||||
|
||||
if (response === false) return;
|
||||
|
||||
if (response !== true) responses.push(response as string);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attachment of message.attachments.values()) {
|
||||
const match = md5Regex.exec(attachment.name);
|
||||
md5Regex.lastIndex = 0;
|
||||
|
||||
const md5s: string[] = [];
|
||||
|
||||
if (match) md5s.push(match[1]);
|
||||
else if (ALLOWED_MIMETYPES.includes(attachment.contentType!)) {
|
||||
const md5Data = await calculateMD5FromURL(attachment.url);
|
||||
if (!md5Data) continue;
|
||||
md5s.push(md5Data.correctedFileMD5, md5Data.originalFileMD5);
|
||||
}
|
||||
|
||||
if (md5s.length == 0) continue;
|
||||
|
||||
for (const md5 of md5s) {
|
||||
const post = await getE621PostByMd5(md5);
|
||||
|
||||
if (post) {
|
||||
if (await blacklistIfNecessary(message, [post])) return;
|
||||
|
||||
responses.push(`<${getPostUrl(post)}>`);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
await message.reply(responses.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMessageUpdate(oldMessage: Message | PartialMessage, newMessage: Message) {
|
||||
if (newMessage.author.bot) return;
|
||||
|
||||
const loggedMessage = await Database.getMessageWithRetry(newMessage.id);
|
||||
|
||||
if (!loggedMessage) {
|
||||
if (newMessage.inGuild()) await Database.putMessage(newMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMessage.inGuild() && isEdited(loggedMessage, newMessage)) {
|
||||
await Database.putMessage(newMessage);
|
||||
await logEdit(loggedMessage, newMessage);
|
||||
}
|
||||
|
||||
if (loggedMessage.content == newMessage.content) return;
|
||||
|
||||
const responses: string[] = [];
|
||||
|
||||
for (const test of regexTesters) {
|
||||
if (config.DEV_MODE && !test.runInDev) continue;
|
||||
|
||||
const hasMatches = test.regex.test(newMessage.content);
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
if (hasMatches) {
|
||||
const oldMatches: RegExpExecArray[] = [];
|
||||
const newMatches: RegExpExecArray[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = test.regex.exec(newMessage.content)) != null) {
|
||||
newMatches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
while ((match = test.regex.exec(loggedMessage.content)) != null) {
|
||||
oldMatches.push(match);
|
||||
}
|
||||
test.regex.lastIndex = 0;
|
||||
|
||||
const properMatches: RegExpExecArray[] = [];
|
||||
|
||||
for (const newMatch of newMatches) {
|
||||
if (!oldMatches.find(m => m[1] == newMatch[1])) properMatches.push(newMatch);
|
||||
}
|
||||
|
||||
if (properMatches.length == 0) continue;
|
||||
|
||||
const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches));
|
||||
|
||||
if (response === false) return;
|
||||
|
||||
if (response !== true) responses.push(response as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
await newMessage.reply(responses.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMessageDelete(message: Message | PartialMessage) {
|
||||
const loggedMessage = await Database.getMessageWithRetry(message.id);
|
||||
|
||||
if (!loggedMessage) return;
|
||||
|
||||
if (message.inGuild()) await logDeletion(loggedMessage, message);
|
||||
}
|
||||
|
||||
export async function handleBulkMessageDelete(messages: ReadonlyCollection<string, Message | Partial>, channel: GuildTextBasedChannel) {
|
||||
for (const message of messages.values()) {
|
||||
await handleMessageDelete(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function searchHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `<${config.E621_BASE_URL}/posts?tags=${encodeURIComponent(group[1])}>\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function wikiPageHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `<${config.E621_BASE_URL}/wiki_pages/${group[1].split('#').map(t => encodeURIComponent(t)).join('#')}>\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function blacklistIfNecessary(message: Message, posts: E621Post[]): Promise<boolean> {
|
||||
const blacklistedIds: number[] = [];
|
||||
|
||||
const channel = await message.channel.fetch() as GuildTextBasedChannel;
|
||||
|
||||
const isStaffChannel = await channelIsInStaffCategory(channel);
|
||||
|
||||
for (const post of posts) {
|
||||
if (spoilerOrBlacklist(post).action == PostAction.Blacklist) {
|
||||
blacklistedIds.push(post.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (blacklistedIds.length == 0) return false;
|
||||
|
||||
await message.delete();
|
||||
|
||||
if (channel.parentId && isStaffChannel) {
|
||||
await message.channel.send({
|
||||
content: `_sucks message into the void._ ${message.author.toString()} nono, don't post links to ${blacklistedIds.length == 1 ? `post ${blacklistedIds[0]}` : `posts \`${blacklistedIds.join('`, `')}\``}. See rule #5.b for more details.`,
|
||||
allowedMentions: {
|
||||
users: [message.author.id]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await message.channel.send({
|
||||
content: `_sucks message into the void._ ${message.author.toString()} nono, don't post links to young/cub content. See rule #5.b for more details.`,
|
||||
allowedMentions: {
|
||||
users: [message.author.id]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function postIdHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: { post: E621Post, spoilered: boolean }[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621Post(match[1]);
|
||||
if (post) posts.push({
|
||||
spoilered: isInSpoilerTags(message.content, match.index),
|
||||
post
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts.map(p => p.post))) return false;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const sfw = await channelIsSafe(message.channel as GuildBasedChannel);
|
||||
|
||||
const content = posts.map((postData) => {
|
||||
if (sfw && postData.post.rating != 's') return ` [NSFW] <${getPostUrl(postData.post)}>`;
|
||||
|
||||
const shouldSpoiler = spoilerOrBlacklist(postData.post);
|
||||
if (shouldSpoiler.action == PostAction.Spoiler) return `${spoiler(getPostUrl(postData.post))} (${shouldSpoiler.tag})`;
|
||||
|
||||
return postData.spoilered ? spoiler(getPostUrl(postData.post)) : getPostUrl(postData.post);
|
||||
}).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function idHandler(path: string, message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const content = matchedGroups.map(m => `${config.E621_BASE_URL}/${path}/${m[1]}`).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function postHandler(transform: ((idString: string) => number) | null, message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: E621Post[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621Post(transform ? transform(match[1]) : match[1]);
|
||||
if (post) posts.push(post);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function imageHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
if (!message.guildId) return true;
|
||||
|
||||
const posts: E621Post[] = [];
|
||||
|
||||
for (const match of matchedGroups) {
|
||||
try {
|
||||
const post = await getE621PostByMd5(match[1]);
|
||||
if (post) posts.push(post);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await blacklistIfNecessary(message, posts)) return false;
|
||||
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
const content = posts.map(post => `<${getPostUrl(post)}>`).join('\n');
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function githubPullRequestHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `${config.GIT_REPO_BASE_URL}/pull/${group[1]}\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function githubIssueHandler(message: Message, matchedGroups: RegExpExecArray[]): Promise<string | boolean> {
|
||||
const skip = await channelIgnoresLinks(message.channel as GuildBasedChannel);
|
||||
|
||||
if (skip) return true;
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const group of matchedGroups) {
|
||||
content += `${config.GIT_REPO_BASE_URL}/issues/${group[1]}\n`;
|
||||
}
|
||||
|
||||
if (content.trim().length > 0) return content.trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { AnyThreadChannel } from 'discord.js';
|
||||
|
||||
export async function handleThreadCreate(thread: AnyThreadChannel, newlyCreated: boolean) {
|
||||
try {
|
||||
await thread.join();
|
||||
} catch (e) {
|
||||
console.error('Failed to join thread:');
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
import { AnyThreadChannel } from 'discord.js';
|
||||
|
||||
export async function handleThreadCreate(thread: AnyThreadChannel, newlyCreated: boolean) {
|
||||
try {
|
||||
await thread.join();
|
||||
} catch (e) {
|
||||
console.error('Failed to join thread:');
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
import { Guild, GuildMember, GuildTextBasedChannel, time, VoiceBasedChannel, VoiceState } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) {
|
||||
// The logChannel declaration being inside is purposeful, as this event is fired a lot for users talking.
|
||||
if (newState.channelId != null && oldState.channelId != null && newState.channelId != oldState.channelId) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendMovedMessage(logChannel, newState.member!, oldState.channel!, newState.channel!);
|
||||
} else if (oldState.channelId == null && newState.channelId != null) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendJoinMessage(logChannel, newState.member!, newState.channel!);
|
||||
} else if (newState.channelId == null && oldState.channelId != null) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendLeftMessage(logChannel, newState.member!, oldState.channel!);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendJoinMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} joined ${voiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function sendLeftMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} left ${voiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function sendMovedMessage(channel: GuildTextBasedChannel, member: GuildMember, oldVoiceChannel: VoiceBasedChannel, newVoiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} moved from ${oldVoiceChannel} to ${newVoiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> {
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings || !settings.voice_logs_channel_id) return;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.voice_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
return channel as GuildTextBasedChannel;
|
||||
import { Guild, GuildMember, GuildTextBasedChannel, time, VoiceBasedChannel, VoiceState } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) {
|
||||
// The logChannel declaration being inside is purposeful, as this event is fired a lot for users talking.
|
||||
if (newState.channelId != null && oldState.channelId != null && newState.channelId != oldState.channelId) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendMovedMessage(logChannel, newState.member!, oldState.channel!, newState.channel!);
|
||||
} else if (oldState.channelId == null && newState.channelId != null) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendJoinMessage(logChannel, newState.member!, newState.channel!);
|
||||
} else if (newState.channelId == null && oldState.channelId != null) {
|
||||
const logChannel = await getVoiceLogsChannel(newState.guild);
|
||||
if (!logChannel) return;
|
||||
|
||||
await sendLeftMessage(logChannel, newState.member!, oldState.channel!);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendJoinMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} joined ${voiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function sendLeftMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} left ${voiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function sendMovedMessage(channel: GuildTextBasedChannel, member: GuildMember, oldVoiceChannel: VoiceBasedChannel, newVoiceChannel: VoiceBasedChannel) {
|
||||
await channel.send(`${member} moved from ${oldVoiceChannel} to ${newVoiceChannel} at ${time()}`);
|
||||
}
|
||||
|
||||
async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> {
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings || !settings.voice_logs_channel_id) return;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.voice_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
return channel as GuildTextBasedChannel;
|
||||
}
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
export * from './handle-audit-log-create';
|
||||
export * from './handle-ban-remove';
|
||||
export * from './handle-guild-create';
|
||||
export * from './handle-member-join';
|
||||
export * from './handle-message';
|
||||
export * from './handle-thread-create';
|
||||
export * from './handle-voice-state-update';
|
||||
export * from './handle-audit-log-create';
|
||||
export * from './handle-ban-remove';
|
||||
export * from './handle-guild-create';
|
||||
export * from './handle-member-join';
|
||||
export * from './handle-message';
|
||||
export * from './handle-thread-create';
|
||||
export * from './handle-voice-state-update';
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'add-knowledgebase-item-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const name = interaction.fields.getTextInputValue('name');
|
||||
const content = interaction.fields.getTextInputValue('content');
|
||||
|
||||
if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.');
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebaseByName(interaction.guild.id, name);
|
||||
|
||||
if (existingItem) return interaction.editReply(`Knowledgebase item ${name} already exists!`);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
value: content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.addToKnowledgebase(interaction.guild.id, name, content);
|
||||
|
||||
return interaction.editReply(`Entry \`${name}\` added to knowledgebase.`);
|
||||
}
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'add-knowledgebase-item-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const name = interaction.fields.getTextInputValue('name');
|
||||
const content = interaction.fields.getTextInputValue('content');
|
||||
|
||||
if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.');
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebaseByName(interaction.guild.id, name);
|
||||
|
||||
if (existingItem) return interaction.editReply(`Knowledgebase item ${name} already exists!`);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
value: content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.addToKnowledgebase(interaction.guild.id, name, content);
|
||||
|
||||
return interaction.editReply(`Entry \`${name}\` added to knowledgebase.`);
|
||||
}
|
||||
};
|
||||
@@ -1,40 +1,40 @@
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'add-note-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) {
|
||||
const message = interaction.fields.getTextInputValue('note-message');
|
||||
|
||||
const user = await client.users.fetch(id);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: message,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.putNote(id, message, interaction.user.id);
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Note added' });
|
||||
}
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'add-note-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) {
|
||||
const message = interaction.fields.getTextInputValue('note-message');
|
||||
|
||||
const user = await client.users.fetch(id);
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Note Added',
|
||||
description: null,
|
||||
color: 0x00FF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Moderator',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${id}>\n${user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: message,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.putNote(id, message, interaction.user.id);
|
||||
|
||||
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Note added' });
|
||||
}
|
||||
};
|
||||
@@ -1,54 +1,54 @@
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'edit-knowledgebase-item-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, idString: string) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const id = parseInt(idString);
|
||||
const content = interaction.fields.getTextInputValue('content');
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!existingItem) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Edited',
|
||||
description: null,
|
||||
color: 0xFFFF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: existingItem.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Old Content',
|
||||
value: existingItem.content,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'New Content',
|
||||
value: content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.editKnowledgebaseItem(id, content);
|
||||
|
||||
return interaction.editReply(`Edited knowledgebase entry \`${existingItem.name}\`.`);
|
||||
}
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { deferInteraction, logCustomEvent } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'edit-knowledgebase-item-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, idString: string) {
|
||||
if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' });
|
||||
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const id = parseInt(idString);
|
||||
const content = interaction.fields.getTextInputValue('content');
|
||||
|
||||
const existingItem = await Database.getFromKnowledgebase(id);
|
||||
|
||||
if (!existingItem) return interaction.editReply('Knowledgebase item not found.');
|
||||
|
||||
if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.');
|
||||
|
||||
logCustomEvent(interaction.guild!, {
|
||||
title: 'Knowledgebase Item Edited',
|
||||
description: null,
|
||||
color: 0xFFFF00,
|
||||
timestamp: new Date(),
|
||||
fields: [
|
||||
{
|
||||
name: 'User',
|
||||
value: `<@${interaction.user.id}>\n${interaction.user.username}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
value: existingItem.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Old Content',
|
||||
value: existingItem.content,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'New Content',
|
||||
value: content,
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await Database.editKnowledgebaseItem(id, content);
|
||||
|
||||
return interaction.editReply(`Edited knowledgebase entry \`${existingItem.name}\`.`);
|
||||
}
|
||||
};
|
||||
@@ -1,31 +1,31 @@
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
const warning = '\n\n\nLeaving this thread without acknowledgement may result in punishment. Staff will close the thread when they deem your response acceptable.';
|
||||
|
||||
export default {
|
||||
name: 'open-mod-ticket',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, userId: string) {
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(userId);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_channel_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' });
|
||||
|
||||
const title = interaction.fields.getTextInputValue('title') ? interaction.fields.getTextInputValue('title') : `Mod Ticket For ${member.displayName}`;
|
||||
const reason = interaction.fields.getTextInputValue('initial-message') + warning;
|
||||
const autoJoin = interaction.fields.getStringSelectValues('auto-join-thread')[0] == 'yes';
|
||||
|
||||
const membersToAdd = [userId];
|
||||
|
||||
if (autoJoin) membersToAdd.push(interaction.user.id);
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, null, reason, title, membersToAdd);
|
||||
|
||||
if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Mod ticket created: ${thread}.${!autoJoin ? "You've selected not to auto join the thread. You will not be notified of messages sent there. You will have to check the thread periodically for user response." : ''}` });
|
||||
else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' });
|
||||
}
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
const warning = '\n\n\nLeaving this thread without acknowledgement may result in punishment. Staff will close the thread when they deem your response acceptable.';
|
||||
|
||||
export default {
|
||||
name: 'open-mod-ticket',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, userId: string) {
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(userId);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_channel_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' });
|
||||
|
||||
const title = interaction.fields.getTextInputValue('title') ? interaction.fields.getTextInputValue('title') : `Mod Ticket For ${member.displayName}`;
|
||||
const reason = interaction.fields.getTextInputValue('initial-message') + warning;
|
||||
const autoJoin = interaction.fields.getStringSelectValues('auto-join-thread')[0] == 'yes';
|
||||
|
||||
const membersToAdd = [userId];
|
||||
|
||||
if (autoJoin) membersToAdd.push(interaction.user.id);
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, null, reason, title, membersToAdd);
|
||||
|
||||
if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Mod ticket created: ${thread}.${!autoJoin ? "You've selected not to auto join the thread. You will not be notified of messages sent there. You will have to check the thread periodically for user response." : ''}` });
|
||||
else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' });
|
||||
}
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'open-ticket-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction) {
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' });
|
||||
|
||||
const reason = interaction.fields.getTextInputValue('ticket-message');
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, member, reason);
|
||||
|
||||
if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Your ticket has been created: ${thread}` });
|
||||
else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' });
|
||||
}
|
||||
import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'open-ticket-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction) {
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_role_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' });
|
||||
|
||||
const reason = interaction.fields.getTextInputValue('ticket-message');
|
||||
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, member, reason);
|
||||
|
||||
if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Your ticket has been created: ${thread}` });
|
||||
else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' });
|
||||
}
|
||||
};
|
||||
+117
-117
@@ -1,118 +1,118 @@
|
||||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, GuildTextBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { canOpenPrivateHelpTicket, createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'report-message',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, channelId: string, messageId: string) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
const reportedMessageChannel = await guild.channels.fetch(channelId) as GuildTextBasedChannel;
|
||||
const reportedMessage = await reportedMessageChannel?.messages.fetch(messageId);
|
||||
|
||||
const additionalInfo = interaction.fields.getTextInputValue('additional-info');
|
||||
const createPrivateHelpTicket = interaction.fields.getStringSelectValues('create-private-help-ticket')[0] == 'yes';
|
||||
|
||||
if (!reportedMessageChannel || !reportedMessage)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to submit report. Please report this to a staff member.' });
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!guildSettings || !guildSettings.moderator_channel_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' });
|
||||
|
||||
const reportsChannel = await interaction.guild!.channels.fetch(guildSettings.moderator_channel_id);
|
||||
|
||||
if (!reportsChannel || !reportsChannel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('New Message Report!')
|
||||
.setColor(0xFF0000)
|
||||
.addFields(
|
||||
{
|
||||
name: 'Message',
|
||||
value: reportedMessage.url,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Message Author',
|
||||
value: reportedMessage.author.toString(),
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Reporter',
|
||||
value: member.toString(),
|
||||
inline: false
|
||||
});
|
||||
|
||||
if (additionalInfo) {
|
||||
embed.addFields(
|
||||
{
|
||||
name: 'Additional Information',
|
||||
value: additionalInfo,
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let replyContent = "Thanks for making a report! I've notified the moderators who can take further action.";
|
||||
|
||||
const wantsTicketButCantOpen = createPrivateHelpTicket && !await canOpenPrivateHelpTicket(member.id);
|
||||
|
||||
if (wantsTicketButCantOpen) {
|
||||
embed.addFields(
|
||||
{
|
||||
name: 'User Requested Private Ticket',
|
||||
value: 'The user requested a private ticket be opened, but already has an open ticket. If additional information is needed, press the button below to open a ticket with the user.',
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
|
||||
replyContent += " Could not open private help ticket since you already have one open. Staff have been notified that you'd like to have a ticket opened, and can make one for you if they deem it necessary.";
|
||||
}
|
||||
|
||||
const openTicketButton = new ButtonBuilder()
|
||||
.setCustomId('open-ticket-for-reported-message')
|
||||
.setLabel('Open Private Ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(openTicketButton);
|
||||
|
||||
const reportMessage = await reportsChannel.send({
|
||||
embeds: [embed],
|
||||
components: createPrivateHelpTicket && !wantsTicketButCantOpen ? [] : [row]
|
||||
});
|
||||
|
||||
await reportsChannel.send({ files: [new AttachmentBuilder(Buffer.from(reportedMessage.content), { name: 'message-content.txt' })] });
|
||||
|
||||
if (createPrivateHelpTicket && !wantsTicketButCantOpen) {
|
||||
if (!guildSettings.private_help_channel_id) {
|
||||
replyContent += ' Could not open private help ticket. Private help channel not set. Please report this to a staff member.';
|
||||
} else {
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, member, `Ticket created with message report (${reportMessage.url}). ${member}, use this channel to talk with staff privately about the reported message (${reportedMessage.url}). ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`);
|
||||
if (thread) {
|
||||
replyContent += ` Private help ticket created: ${thread}.`;
|
||||
embed.addFields({
|
||||
name: 'Private Help Ticket',
|
||||
value: thread.url,
|
||||
inline: false
|
||||
});
|
||||
|
||||
await reportMessage.edit({ embeds: [embed] });
|
||||
|
||||
} else {
|
||||
replyContent += ' There was an issue opening a private help ticket, please report this to a staff member.';
|
||||
await reportMessage.edit({
|
||||
embeds: [embed],
|
||||
components: [row]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply(replyContent);
|
||||
}
|
||||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, GuildTextBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { canOpenPrivateHelpTicket, createPrivateHelpTicketThread } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'report-message',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, channelId: string, messageId: string) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
const guild = await client.guilds.fetch(interaction.guildId!);
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
const reportedMessageChannel = await guild.channels.fetch(channelId) as GuildTextBasedChannel;
|
||||
const reportedMessage = await reportedMessageChannel?.messages.fetch(messageId);
|
||||
|
||||
const additionalInfo = interaction.fields.getTextInputValue('additional-info');
|
||||
const createPrivateHelpTicket = interaction.fields.getStringSelectValues('create-private-help-ticket')[0] == 'yes';
|
||||
|
||||
if (!reportedMessageChannel || !reportedMessage)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to submit report. Please report this to a staff member.' });
|
||||
|
||||
const guildSettings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!guildSettings || !guildSettings.moderator_channel_id)
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' });
|
||||
|
||||
const reportsChannel = await interaction.guild!.channels.fetch(guildSettings.moderator_channel_id);
|
||||
|
||||
if (!reportsChannel || !reportsChannel.isSendable())
|
||||
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('New Message Report!')
|
||||
.setColor(0xFF0000)
|
||||
.addFields(
|
||||
{
|
||||
name: 'Message',
|
||||
value: reportedMessage.url,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Message Author',
|
||||
value: reportedMessage.author.toString(),
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Reporter',
|
||||
value: member.toString(),
|
||||
inline: false
|
||||
});
|
||||
|
||||
if (additionalInfo) {
|
||||
embed.addFields(
|
||||
{
|
||||
name: 'Additional Information',
|
||||
value: additionalInfo,
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let replyContent = "Thanks for making a report! I've notified the moderators who can take further action.";
|
||||
|
||||
const wantsTicketButCantOpen = createPrivateHelpTicket && !await canOpenPrivateHelpTicket(member.id);
|
||||
|
||||
if (wantsTicketButCantOpen) {
|
||||
embed.addFields(
|
||||
{
|
||||
name: 'User Requested Private Ticket',
|
||||
value: 'The user requested a private ticket be opened, but already has an open ticket. If additional information is needed, press the button below to open a ticket with the user.',
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
|
||||
replyContent += " Could not open private help ticket since you already have one open. Staff have been notified that you'd like to have a ticket opened, and can make one for you if they deem it necessary.";
|
||||
}
|
||||
|
||||
const openTicketButton = new ButtonBuilder()
|
||||
.setCustomId('open-ticket-for-reported-message')
|
||||
.setLabel('Open Private Ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(openTicketButton);
|
||||
|
||||
const reportMessage = await reportsChannel.send({
|
||||
embeds: [embed],
|
||||
components: createPrivateHelpTicket && !wantsTicketButCantOpen ? [] : [row]
|
||||
});
|
||||
|
||||
await reportsChannel.send({ files: [new AttachmentBuilder(Buffer.from(reportedMessage.content), { name: 'message-content.txt' })] });
|
||||
|
||||
if (createPrivateHelpTicket && !wantsTicketButCantOpen) {
|
||||
if (!guildSettings.private_help_channel_id) {
|
||||
replyContent += ' Could not open private help ticket. Private help channel not set. Please report this to a staff member.';
|
||||
} else {
|
||||
const thread = await createPrivateHelpTicketThread(client, guild, member, `Ticket created with message report (${reportMessage.url}). ${member}, use this channel to talk with staff privately about the reported message (${reportedMessage.url}). ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`);
|
||||
if (thread) {
|
||||
replyContent += ` Private help ticket created: ${thread}.`;
|
||||
embed.addFields({
|
||||
name: 'Private Help Ticket',
|
||||
value: thread.url,
|
||||
inline: false
|
||||
});
|
||||
|
||||
await reportMessage.edit({ embeds: [embed] });
|
||||
|
||||
} else {
|
||||
replyContent += ' There was an issue opening a private help ticket, please report this to a staff member.';
|
||||
await reportMessage.edit({
|
||||
embeds: [embed],
|
||||
components: [row]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply(replyContent);
|
||||
}
|
||||
};
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Client, ModalSubmitInteraction } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { deferInteraction, syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'sync-name-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const e621Id = Number(interaction.fields.getTextInputValue('id') ?? 0);
|
||||
|
||||
console.log(e621Id);
|
||||
|
||||
if (isNaN(e621Id)) return await interaction.editReply('Provided id is not a number');
|
||||
|
||||
const member = await interaction.guild?.members.fetch(id);
|
||||
|
||||
if (!member) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
if (!guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, e621Id);
|
||||
}
|
||||
import { Client, ModalSubmitInteraction } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { deferInteraction, syncName } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'sync-name-modal',
|
||||
handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) {
|
||||
await deferInteraction(interaction);
|
||||
|
||||
const e621Id = Number(interaction.fields.getTextInputValue('id') ?? 0);
|
||||
|
||||
console.log(e621Id);
|
||||
|
||||
if (isNaN(e621Id)) return await interaction.editReply('Provided id is not a number');
|
||||
|
||||
const member = await interaction.guild?.members.fetch(id);
|
||||
|
||||
if (!member) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
if (!guild.members.me) {
|
||||
return interaction.editReply('An error has occurred. Please try again later.');
|
||||
}
|
||||
|
||||
await syncName(interaction, member, e621Id);
|
||||
}
|
||||
};
|
||||
+483
-483
@@ -1,484 +1,484 @@
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { open, Database as SqliteDatabase } from 'sqlite';
|
||||
import { serializeMessage, wait } from '../utils';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket } from '../types';
|
||||
import { Message } from '../events';
|
||||
|
||||
const DB_SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS discord_names (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
discord_id TEXT NOT NULL,
|
||||
discord_username TEXT NOT NULL,
|
||||
added_on datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
general_chat_id TEXT,
|
||||
new_member_channel_id TEXT,
|
||||
tickets_channel_id TEXT,
|
||||
event_logs_channel_id TEXT,
|
||||
discord_logs_channel_id TEXT,
|
||||
audit_logs_channel_id TEXT,
|
||||
voice_logs_channel_id TEXT,
|
||||
admin_role_id TEXT,
|
||||
private_help_role_id TEXT,
|
||||
devwatch_role_id TEXT,
|
||||
staff_categories TEXT,
|
||||
safe_channels TEXT,
|
||||
link_skip_channels TEXT,
|
||||
github_release_channel TEXT,
|
||||
moderator_channel_id TEXT,
|
||||
private_help_channel_id TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY ON CONFLICT REPLACE,
|
||||
author_id TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
attachments TEXT NOT NULL,
|
||||
stickers TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_authors ON messages (author_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_channels ON messages (channel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_phrases (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
phrase TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
reason TEXT,
|
||||
mod_id TEXT,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_user_ids ON notes (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note_edits (
|
||||
id INTEGER PRIMARY KEY,
|
||||
note_id INTEGER,
|
||||
mod_id TEXT,
|
||||
previous_reason TEXT,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY(note_id) REFERENCES notes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bans (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
expires INTEGER,
|
||||
expires_at datetime,
|
||||
full_ban INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS github_user_mapping (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id TEXT,
|
||||
github_username TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledgebase (
|
||||
id INTEGER PRIMARY KEY,
|
||||
guild_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS private_help_tickets (
|
||||
id INTEGER PRIMARY KEY,
|
||||
thread_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_timestamp ON private_help_tickets (timestamp);
|
||||
`;
|
||||
|
||||
export const enum PrivateHelpTicketStatus {
|
||||
OPEN = 0,
|
||||
CLOSED = 1
|
||||
}
|
||||
|
||||
export class Database {
|
||||
private static db: SqliteDatabase;
|
||||
|
||||
static async open(file: string): Promise<void> {
|
||||
if (Database.db) return;
|
||||
|
||||
Database.db = await open({
|
||||
filename: file,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
console.log('SQLite database opened');
|
||||
|
||||
await Database.ensure();
|
||||
}
|
||||
|
||||
private static async ensure() {
|
||||
await Database.db.exec(DB_SCHEMA);
|
||||
console.log('SQLite database ensured');
|
||||
}
|
||||
|
||||
// -- START WHOIS --
|
||||
|
||||
static async getE621Ids(discordId: string): Promise<number[]> {
|
||||
const ids = await Database.db.all<{ user_id: number }[]>('SELECT DISTINCT user_id FROM discord_names WHERE discord_id = ?', discordId);
|
||||
|
||||
return ids.map(r => r.user_id);
|
||||
}
|
||||
|
||||
static async getDiscordIds(e621Id: string | number): Promise<string[]> {
|
||||
// Perhaps this would be better and then we can return all the data: SELECT * FROM (SELECT * FROM discord_names WHERE user_id = ? ORDER BY id DESC) GROUP BY discord_id;
|
||||
const ids = await Database.db.all<{ discord_id: string }[]>('SELECT DISTINCT discord_id FROM discord_names WHERE user_id = ?', e621Id);
|
||||
|
||||
return ids.map(r => r.discord_id);
|
||||
}
|
||||
|
||||
static async getCombinedIds(id: string): Promise<{ userId: string, discordId: string }[]> {
|
||||
const ids = await Database.db.all<{ discord_id: string, user_id: number }[]>(`
|
||||
WITH RECURSIVE rec AS (
|
||||
SELECT DISTINCT d1.user_id, d1.discord_id, 1 AS depth FROM discord_names d1 WHERE d1.user_id = ? or d1.discord_id = ?
|
||||
UNION
|
||||
SELECT d3.user_id, d3.discord_id, depth + 1 AS depth FROM rec
|
||||
LEFT OUTER JOIN discord_names d2 ON rec.discord_id = d2.discord_id
|
||||
LEFT OUTER JOIN discord_names d3 ON d2.user_id = d3.user_id
|
||||
WHERE depth <= 5 AND rec.depth = depth
|
||||
) SELECT DISTINCT user_id, discord_id FROM rec`, id, id);
|
||||
|
||||
return ids.map(r => ({ userId: r.user_id.toString(), discordId: r.discord_id }));
|
||||
}
|
||||
|
||||
static async putUser(id: number, user: { id: string, username: string }) {
|
||||
await Database.db.run('INSERT INTO discord_names(user_id, discord_id, discord_username) VALUES (?, ?, ?)', id, user.id, user.username);
|
||||
}
|
||||
|
||||
static async removeUser(id: number, discordId: string) {
|
||||
await Database.db.run('DELETE from discord_names WHERE user_id = ? AND discord_id = ?', id, discordId);
|
||||
}
|
||||
|
||||
// -- END WHOIS --
|
||||
|
||||
// -- START SETTINGS --
|
||||
|
||||
static async getGuildSettings(guildId: string): Promise<GuildSettings | undefined> {
|
||||
return await Database.db.get<GuildSettings>('SELECT * FROM settings WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
static async putGuild(guildId: string) {
|
||||
await Database.db.run('INSERT INTO settings(guild_id) VALUES (?)', guildId);
|
||||
}
|
||||
|
||||
static async setGuildGeneralChatId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET general_chat_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildTicketsLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET tickets_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildEventsLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET event_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildDiscordLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET discord_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildAuditLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET audit_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildVoiceLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET voice_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildNewMemberLogsChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET new_member_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildModeratorChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET moderator_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildAdminRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET admin_role_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildPrivateHelperRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET private_help_role_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildDevWatchRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET devwatch_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);
|
||||
}
|
||||
|
||||
static async setPrivateHelpChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET private_help_channel_id = ? 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[]> {
|
||||
const settings = await Database.db.get<{ [setting]: string }>(`SELECT ${setting} FROM settings WHERE guild_id = ?`, guildId);
|
||||
|
||||
if (!settings || !settings[setting]) return [];
|
||||
|
||||
return settings[setting].split(',');
|
||||
}
|
||||
|
||||
static async putGuildArraySetting(setting: GuildArraySetting, guildId: string, value: string) {
|
||||
const values = await Database.getGuildArraySetting(setting, guildId);
|
||||
|
||||
if (values.indexOf(value) == -1) values.push(value);
|
||||
|
||||
const newString = values.join(',');
|
||||
|
||||
await Database.db.run(`UPDATE settings SET ${setting} = ? WHERE guild_id = ?`, newString, guildId);
|
||||
}
|
||||
|
||||
static async removeGuildArraySetting(setting: GuildArraySetting, guildId: string, value: string): Promise<boolean> {
|
||||
const values = await Database.getGuildArraySetting(setting, guildId);
|
||||
|
||||
const index = values.indexOf(value);
|
||||
if (index == -1) return false;
|
||||
|
||||
values.splice(index, 1);
|
||||
|
||||
const newString = values.join(',');
|
||||
|
||||
await Database.db.run(`UPDATE settings SET ${setting} = ? WHERE guild_id = ?`, newString, guildId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -- END SETTINGS --
|
||||
|
||||
// START MESSAGE LOGS --
|
||||
|
||||
static async putMessage(message: Message): Promise<boolean> {
|
||||
try {
|
||||
const serializedMessage = serializeMessage(message);
|
||||
|
||||
await Database.db.run(`
|
||||
INSERT INTO messages (id, author_id, author_name, channel_id, attachments, stickers, content) VALUES
|
||||
(:id, :author_id, :author_name, :channel_id, :attachments, :stickers, :content)
|
||||
`, ...serializedMessage);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getMessage(id: string): Promise<LoggedMessage | undefined> {
|
||||
return await Database.db.get<LoggedMessage>('SELECT * FROM messages WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async getMessageWithRetry(id: string, retries = 5, delay = 500): Promise<LoggedMessage | undefined> {
|
||||
let tried = 0;
|
||||
while (tried < retries) {
|
||||
tried++;
|
||||
const message = await Database.db.get<LoggedMessage>('SELECT * FROM messages WHERE id = ?', id);
|
||||
|
||||
if (message) return message;
|
||||
|
||||
await wait(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// -- END MESSAGE LOGS --
|
||||
|
||||
// -- START TICKETS --
|
||||
|
||||
static async putTicket(ticketId: number, messageId: string) {
|
||||
await Database.db.run('INSERT INTO tickets(id, message_id) VALUES (?, ?)', ticketId, messageId);
|
||||
}
|
||||
|
||||
static async removeTicket(ticketId: number) {
|
||||
await Database.db.run('DELETE from tickets WHERE id = ?', ticketId);
|
||||
}
|
||||
|
||||
static async getTicketMessageId(ticketId: number): Promise<string | undefined> {
|
||||
const ticket = await Database.db.get<Pick<TicketMessage, 'message_id'>>('SELECT message_id FROM tickets WHERE id = ?', ticketId);
|
||||
return ticket?.message_id;
|
||||
}
|
||||
|
||||
static async putTicketPhrase(userId: string, phrase: string) {
|
||||
await Database.db.run('INSERT INTO ticket_phrases(user_id, phrase) VALUES (?, ?)', userId, phrase);
|
||||
}
|
||||
|
||||
static async getTicketPhrase(id: number): Promise<TicketPhrase | undefined> {
|
||||
return await Database.db.get<TicketPhrase>('SELECT * FROM ticket_phrases WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async removeTicketPhrase(id: number) {
|
||||
await Database.db.run('DELETE from ticket_phrases WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async removeAllTicketPhrasesFor(id: string): Promise<number> {
|
||||
return (await Database.db.run('DELETE from ticket_phrases WHERE user_id = ?', id)).changes!;
|
||||
}
|
||||
|
||||
static async getTicketPhrasesFor(userId: string): Promise<TicketPhrase[]> {
|
||||
return await Database.db.all<TicketPhrase[]>('SELECT * from ticket_phrases WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
static async getAllTicketPhrases(cb: (ticketPhrase: TicketPhrase) => void) {
|
||||
await Database.db.each<TicketPhrase>('SELECT * from ticket_phrases', (err: any, ticketPhrase: TicketPhrase) => {
|
||||
if (err) return console.error(err);
|
||||
|
||||
cb(ticketPhrase);
|
||||
});
|
||||
}
|
||||
|
||||
// -- END TICKETS --
|
||||
|
||||
// -- START NOTES --
|
||||
|
||||
static async putNote(userId: string, reason: string, modId: string) {
|
||||
await Database.db.run('INSERT INTO notes(user_id, reason, mod_id) VALUES (?, ?, ?)', userId, reason, modId);
|
||||
}
|
||||
|
||||
static async editNote(id: number, oldReason: string, newReason: string, modId: string) {
|
||||
await Database.db.run('UPDATE notes SET reason = ?, mod_id = ? WHERE id = ?', newReason, modId, id);
|
||||
await Database.db.run('INSERT INTO note_edits(note_id, mod_id, previous_reason) VALUES (?, ?, ?)', id, modId, oldReason);
|
||||
}
|
||||
|
||||
static async removeNote(id: number): Promise<boolean> {
|
||||
const res = await Database.db.run('DELETE from notes WHERE id = ?', id);
|
||||
|
||||
return (res.changes ?? 0) > 0;
|
||||
}
|
||||
|
||||
static async getNotes(userId: string): Promise<Note[]> {
|
||||
return await Database.db.all<Note[]>('SELECT * from notes WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
// -- END NOTES --
|
||||
|
||||
// -- START BANS --
|
||||
|
||||
static async putBan(userId: string, expiresAt: Date | null, fullBan = false) {
|
||||
await Database.db.run('INSERT INTO bans(user_id, expires, expires_at, full_ban) VALUES (?, ?, ?, ?)', userId, expiresAt != null ? 1 : 0, expiresAt, fullBan);
|
||||
}
|
||||
|
||||
static async getBan(userId: string): Promise<Ban | undefined> {
|
||||
return await Database.db.get('SELECT * from bans WHERE user_id = ? ORDER BY id DESC', userId);
|
||||
}
|
||||
|
||||
static async getExpiredBans(date: Date): Promise<Ban[]> {
|
||||
return await Database.db.all<Ban[]>('SELECT * from bans WHERE expires = 1 AND expires_at <= ?', date);
|
||||
}
|
||||
|
||||
static async pruneExpiredBans(date: Date) {
|
||||
await Database.db.all<Ban[]>('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date);
|
||||
}
|
||||
|
||||
static async removeBan(userId: string) {
|
||||
await Database.db.run('DELETE from bans WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
// -- 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 --
|
||||
|
||||
// -- START KNOWLEDGEBASE --
|
||||
|
||||
static async addToKnowledgebase(guildId: string, name: string, content: string) {
|
||||
if (content.length > 2000) return;
|
||||
|
||||
await Database.db.run('INSERT INTO knowledgebase(guild_id, name, content) VALUES (?, ?, ?)', guildId, name, content);
|
||||
}
|
||||
|
||||
static async removeFromKnowledgebase(id: number) {
|
||||
await Database.db.run('DELETE from knowledgebase WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async editKnowledgebaseItem(id: number, content: string) {
|
||||
if (content.length > 2000) return;
|
||||
|
||||
await Database.db.run('UPDATE knowledgebase SET content = ? WHERE id = ?', content, id);
|
||||
}
|
||||
|
||||
static async getFromKnowledgebaseByName(guildId: string, name: string): Promise<KnowledgebaseItem | undefined> {
|
||||
return await Database.db.get<KnowledgebaseItem>('SELECT * from knowledgebase WHERE guild_id = ? AND name = ?', guildId, name);
|
||||
}
|
||||
|
||||
static async getFromKnowledgebase(id: number): Promise<KnowledgebaseItem | undefined> {
|
||||
return await Database.db.get<KnowledgebaseItem>('SELECT * from knowledgebase WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async getAllKnowledgebaseItems(guildId: string): Promise<KnowledgebaseItem[]> {
|
||||
return await Database.db.all<KnowledgebaseItem[]>('SELECT * from knowledgebase WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
// -- END KNOWLEDGEBASE --
|
||||
|
||||
// -- START PRIVATE HELP TICKETS --
|
||||
|
||||
static async createPrivateHelpTicket(userId: string, threadId: string) {
|
||||
await Database.db.run('INSERT INTO private_help_tickets(user_id, thread_id, status) VALUES (?, ?, ?)', userId, threadId, PrivateHelpTicketStatus.OPEN);
|
||||
}
|
||||
|
||||
static async closePrivateHelpTicket(threadId: string) {
|
||||
await Database.db.run('UPDATE private_help_tickets SET status = ? WHERE thread_id = ?', PrivateHelpTicketStatus.CLOSED, threadId);
|
||||
}
|
||||
|
||||
static async getLatestPrivateHelpTicketBy(userId: string): Promise<PrivateHelpTicket | undefined> {
|
||||
return await Database.db.get<PrivateHelpTicket>('SELECT * from private_help_tickets WHERE user_id = ? ORDER BY timestamp DESC LIMIT 1', userId);
|
||||
}
|
||||
|
||||
static async getAllOpenPrivateHelpTickets(): Promise<PrivateHelpTicket[]> {
|
||||
return await Database.db.all<PrivateHelpTicket[]>('SELECT * from private_help_tickets WHERE status = ?', PrivateHelpTicketStatus.OPEN);
|
||||
}
|
||||
|
||||
// -- END PRIVATE HELP TICKETS
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { open, Database as SqliteDatabase } from 'sqlite';
|
||||
import { serializeMessage, wait } from '../utils';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket } from '../types';
|
||||
import { Message } from '../events';
|
||||
|
||||
const DB_SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS discord_names (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
discord_id TEXT NOT NULL,
|
||||
discord_username TEXT NOT NULL,
|
||||
added_on datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
general_chat_id TEXT,
|
||||
new_member_channel_id TEXT,
|
||||
tickets_channel_id TEXT,
|
||||
event_logs_channel_id TEXT,
|
||||
discord_logs_channel_id TEXT,
|
||||
audit_logs_channel_id TEXT,
|
||||
voice_logs_channel_id TEXT,
|
||||
admin_role_id TEXT,
|
||||
private_help_role_id TEXT,
|
||||
devwatch_role_id TEXT,
|
||||
staff_categories TEXT,
|
||||
safe_channels TEXT,
|
||||
link_skip_channels TEXT,
|
||||
github_release_channel TEXT,
|
||||
moderator_channel_id TEXT,
|
||||
private_help_channel_id TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY ON CONFLICT REPLACE,
|
||||
author_id TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL,
|
||||
channel_id TEXT NOT NULL,
|
||||
attachments TEXT NOT NULL,
|
||||
stickers TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_authors ON messages (author_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_channels ON messages (channel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_phrases (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
phrase TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
reason TEXT,
|
||||
mod_id TEXT,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_user_ids ON notes (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note_edits (
|
||||
id INTEGER PRIMARY KEY,
|
||||
note_id INTEGER,
|
||||
mod_id TEXT,
|
||||
previous_reason TEXT,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY(note_id) REFERENCES notes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bans (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
expires INTEGER,
|
||||
expires_at datetime,
|
||||
full_ban INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS github_user_mapping (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id TEXT,
|
||||
github_username TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledgebase (
|
||||
id INTEGER PRIMARY KEY,
|
||||
guild_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS private_help_tickets (
|
||||
id INTEGER PRIMARY KEY,
|
||||
thread_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS index_timestamp ON private_help_tickets (timestamp);
|
||||
`;
|
||||
|
||||
export const enum PrivateHelpTicketStatus {
|
||||
OPEN = 0,
|
||||
CLOSED = 1
|
||||
}
|
||||
|
||||
export class Database {
|
||||
private static db: SqliteDatabase;
|
||||
|
||||
static async open(file: string): Promise<void> {
|
||||
if (Database.db) return;
|
||||
|
||||
Database.db = await open({
|
||||
filename: file,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
console.log('SQLite database opened');
|
||||
|
||||
await Database.ensure();
|
||||
}
|
||||
|
||||
private static async ensure() {
|
||||
await Database.db.exec(DB_SCHEMA);
|
||||
console.log('SQLite database ensured');
|
||||
}
|
||||
|
||||
// -- START WHOIS --
|
||||
|
||||
static async getE621Ids(discordId: string): Promise<number[]> {
|
||||
const ids = await Database.db.all<{ user_id: number }[]>('SELECT DISTINCT user_id FROM discord_names WHERE discord_id = ?', discordId);
|
||||
|
||||
return ids.map(r => r.user_id);
|
||||
}
|
||||
|
||||
static async getDiscordIds(e621Id: string | number): Promise<string[]> {
|
||||
// Perhaps this would be better and then we can return all the data: SELECT * FROM (SELECT * FROM discord_names WHERE user_id = ? ORDER BY id DESC) GROUP BY discord_id;
|
||||
const ids = await Database.db.all<{ discord_id: string }[]>('SELECT DISTINCT discord_id FROM discord_names WHERE user_id = ?', e621Id);
|
||||
|
||||
return ids.map(r => r.discord_id);
|
||||
}
|
||||
|
||||
static async getCombinedIds(id: string): Promise<{ userId: string, discordId: string }[]> {
|
||||
const ids = await Database.db.all<{ discord_id: string, user_id: number }[]>(`
|
||||
WITH RECURSIVE rec AS (
|
||||
SELECT DISTINCT d1.user_id, d1.discord_id, 1 AS depth FROM discord_names d1 WHERE d1.user_id = ? or d1.discord_id = ?
|
||||
UNION
|
||||
SELECT d3.user_id, d3.discord_id, depth + 1 AS depth FROM rec
|
||||
LEFT OUTER JOIN discord_names d2 ON rec.discord_id = d2.discord_id
|
||||
LEFT OUTER JOIN discord_names d3 ON d2.user_id = d3.user_id
|
||||
WHERE depth <= 5 AND rec.depth = depth
|
||||
) SELECT DISTINCT user_id, discord_id FROM rec`, id, id);
|
||||
|
||||
return ids.map(r => ({ userId: r.user_id.toString(), discordId: r.discord_id }));
|
||||
}
|
||||
|
||||
static async putUser(id: number, user: { id: string, username: string }) {
|
||||
await Database.db.run('INSERT INTO discord_names(user_id, discord_id, discord_username) VALUES (?, ?, ?)', id, user.id, user.username);
|
||||
}
|
||||
|
||||
static async removeUser(id: number, discordId: string) {
|
||||
await Database.db.run('DELETE from discord_names WHERE user_id = ? AND discord_id = ?', id, discordId);
|
||||
}
|
||||
|
||||
// -- END WHOIS --
|
||||
|
||||
// -- START SETTINGS --
|
||||
|
||||
static async getGuildSettings(guildId: string): Promise<GuildSettings | undefined> {
|
||||
return await Database.db.get<GuildSettings>('SELECT * FROM settings WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
static async putGuild(guildId: string) {
|
||||
await Database.db.run('INSERT INTO settings(guild_id) VALUES (?)', guildId);
|
||||
}
|
||||
|
||||
static async setGuildGeneralChatId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET general_chat_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildTicketsLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET tickets_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildEventsLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET event_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildDiscordLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET discord_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildAuditLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET audit_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildVoiceLogsChannelId(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET voice_logs_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildNewMemberLogsChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET new_member_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildModeratorChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET moderator_channel_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildAdminRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET admin_role_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildPrivateHelperRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET private_help_role_id = ? WHERE guild_id = ?', id, guildId);
|
||||
}
|
||||
|
||||
static async setGuildDevWatchRole(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET devwatch_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);
|
||||
}
|
||||
|
||||
static async setPrivateHelpChannel(guildId: string, id: string) {
|
||||
await Database.db.run('UPDATE settings SET private_help_channel_id = ? 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[]> {
|
||||
const settings = await Database.db.get<{ [setting]: string }>(`SELECT ${setting} FROM settings WHERE guild_id = ?`, guildId);
|
||||
|
||||
if (!settings || !settings[setting]) return [];
|
||||
|
||||
return settings[setting].split(',');
|
||||
}
|
||||
|
||||
static async putGuildArraySetting(setting: GuildArraySetting, guildId: string, value: string) {
|
||||
const values = await Database.getGuildArraySetting(setting, guildId);
|
||||
|
||||
if (values.indexOf(value) == -1) values.push(value);
|
||||
|
||||
const newString = values.join(',');
|
||||
|
||||
await Database.db.run(`UPDATE settings SET ${setting} = ? WHERE guild_id = ?`, newString, guildId);
|
||||
}
|
||||
|
||||
static async removeGuildArraySetting(setting: GuildArraySetting, guildId: string, value: string): Promise<boolean> {
|
||||
const values = await Database.getGuildArraySetting(setting, guildId);
|
||||
|
||||
const index = values.indexOf(value);
|
||||
if (index == -1) return false;
|
||||
|
||||
values.splice(index, 1);
|
||||
|
||||
const newString = values.join(',');
|
||||
|
||||
await Database.db.run(`UPDATE settings SET ${setting} = ? WHERE guild_id = ?`, newString, guildId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -- END SETTINGS --
|
||||
|
||||
// START MESSAGE LOGS --
|
||||
|
||||
static async putMessage(message: Message): Promise<boolean> {
|
||||
try {
|
||||
const serializedMessage = serializeMessage(message);
|
||||
|
||||
await Database.db.run(`
|
||||
INSERT INTO messages (id, author_id, author_name, channel_id, attachments, stickers, content) VALUES
|
||||
(:id, :author_id, :author_name, :channel_id, :attachments, :stickers, :content)
|
||||
`, ...serializedMessage);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getMessage(id: string): Promise<LoggedMessage | undefined> {
|
||||
return await Database.db.get<LoggedMessage>('SELECT * FROM messages WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async getMessageWithRetry(id: string, retries = 5, delay = 500): Promise<LoggedMessage | undefined> {
|
||||
let tried = 0;
|
||||
while (tried < retries) {
|
||||
tried++;
|
||||
const message = await Database.db.get<LoggedMessage>('SELECT * FROM messages WHERE id = ?', id);
|
||||
|
||||
if (message) return message;
|
||||
|
||||
await wait(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// -- END MESSAGE LOGS --
|
||||
|
||||
// -- START TICKETS --
|
||||
|
||||
static async putTicket(ticketId: number, messageId: string) {
|
||||
await Database.db.run('INSERT INTO tickets(id, message_id) VALUES (?, ?)', ticketId, messageId);
|
||||
}
|
||||
|
||||
static async removeTicket(ticketId: number) {
|
||||
await Database.db.run('DELETE from tickets WHERE id = ?', ticketId);
|
||||
}
|
||||
|
||||
static async getTicketMessageId(ticketId: number): Promise<string | undefined> {
|
||||
const ticket = await Database.db.get<Pick<TicketMessage, 'message_id'>>('SELECT message_id FROM tickets WHERE id = ?', ticketId);
|
||||
return ticket?.message_id;
|
||||
}
|
||||
|
||||
static async putTicketPhrase(userId: string, phrase: string) {
|
||||
await Database.db.run('INSERT INTO ticket_phrases(user_id, phrase) VALUES (?, ?)', userId, phrase);
|
||||
}
|
||||
|
||||
static async getTicketPhrase(id: number): Promise<TicketPhrase | undefined> {
|
||||
return await Database.db.get<TicketPhrase>('SELECT * FROM ticket_phrases WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async removeTicketPhrase(id: number) {
|
||||
await Database.db.run('DELETE from ticket_phrases WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async removeAllTicketPhrasesFor(id: string): Promise<number> {
|
||||
return (await Database.db.run('DELETE from ticket_phrases WHERE user_id = ?', id)).changes!;
|
||||
}
|
||||
|
||||
static async getTicketPhrasesFor(userId: string): Promise<TicketPhrase[]> {
|
||||
return await Database.db.all<TicketPhrase[]>('SELECT * from ticket_phrases WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
static async getAllTicketPhrases(cb: (ticketPhrase: TicketPhrase) => void) {
|
||||
await Database.db.each<TicketPhrase>('SELECT * from ticket_phrases', (err: any, ticketPhrase: TicketPhrase) => {
|
||||
if (err) return console.error(err);
|
||||
|
||||
cb(ticketPhrase);
|
||||
});
|
||||
}
|
||||
|
||||
// -- END TICKETS --
|
||||
|
||||
// -- START NOTES --
|
||||
|
||||
static async putNote(userId: string, reason: string, modId: string) {
|
||||
await Database.db.run('INSERT INTO notes(user_id, reason, mod_id) VALUES (?, ?, ?)', userId, reason, modId);
|
||||
}
|
||||
|
||||
static async editNote(id: number, oldReason: string, newReason: string, modId: string) {
|
||||
await Database.db.run('UPDATE notes SET reason = ?, mod_id = ? WHERE id = ?', newReason, modId, id);
|
||||
await Database.db.run('INSERT INTO note_edits(note_id, mod_id, previous_reason) VALUES (?, ?, ?)', id, modId, oldReason);
|
||||
}
|
||||
|
||||
static async removeNote(id: number): Promise<boolean> {
|
||||
const res = await Database.db.run('DELETE from notes WHERE id = ?', id);
|
||||
|
||||
return (res.changes ?? 0) > 0;
|
||||
}
|
||||
|
||||
static async getNotes(userId: string): Promise<Note[]> {
|
||||
return await Database.db.all<Note[]>('SELECT * from notes WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
// -- END NOTES --
|
||||
|
||||
// -- START BANS --
|
||||
|
||||
static async putBan(userId: string, expiresAt: Date | null, fullBan = false) {
|
||||
await Database.db.run('INSERT INTO bans(user_id, expires, expires_at, full_ban) VALUES (?, ?, ?, ?)', userId, expiresAt != null ? 1 : 0, expiresAt, fullBan);
|
||||
}
|
||||
|
||||
static async getBan(userId: string): Promise<Ban | undefined> {
|
||||
return await Database.db.get('SELECT * from bans WHERE user_id = ? ORDER BY id DESC', userId);
|
||||
}
|
||||
|
||||
static async getExpiredBans(date: Date): Promise<Ban[]> {
|
||||
return await Database.db.all<Ban[]>('SELECT * from bans WHERE expires = 1 AND expires_at <= ?', date);
|
||||
}
|
||||
|
||||
static async pruneExpiredBans(date: Date) {
|
||||
await Database.db.all<Ban[]>('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date);
|
||||
}
|
||||
|
||||
static async removeBan(userId: string) {
|
||||
await Database.db.run('DELETE from bans WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
// -- 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 --
|
||||
|
||||
// -- START KNOWLEDGEBASE --
|
||||
|
||||
static async addToKnowledgebase(guildId: string, name: string, content: string) {
|
||||
if (content.length > 2000) return;
|
||||
|
||||
await Database.db.run('INSERT INTO knowledgebase(guild_id, name, content) VALUES (?, ?, ?)', guildId, name, content);
|
||||
}
|
||||
|
||||
static async removeFromKnowledgebase(id: number) {
|
||||
await Database.db.run('DELETE from knowledgebase WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async editKnowledgebaseItem(id: number, content: string) {
|
||||
if (content.length > 2000) return;
|
||||
|
||||
await Database.db.run('UPDATE knowledgebase SET content = ? WHERE id = ?', content, id);
|
||||
}
|
||||
|
||||
static async getFromKnowledgebaseByName(guildId: string, name: string): Promise<KnowledgebaseItem | undefined> {
|
||||
return await Database.db.get<KnowledgebaseItem>('SELECT * from knowledgebase WHERE guild_id = ? AND name = ?', guildId, name);
|
||||
}
|
||||
|
||||
static async getFromKnowledgebase(id: number): Promise<KnowledgebaseItem | undefined> {
|
||||
return await Database.db.get<KnowledgebaseItem>('SELECT * from knowledgebase WHERE id = ?', id);
|
||||
}
|
||||
|
||||
static async getAllKnowledgebaseItems(guildId: string): Promise<KnowledgebaseItem[]> {
|
||||
return await Database.db.all<KnowledgebaseItem[]>('SELECT * from knowledgebase WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
// -- END KNOWLEDGEBASE --
|
||||
|
||||
// -- START PRIVATE HELP TICKETS --
|
||||
|
||||
static async createPrivateHelpTicket(userId: string, threadId: string) {
|
||||
await Database.db.run('INSERT INTO private_help_tickets(user_id, thread_id, status) VALUES (?, ?, ?)', userId, threadId, PrivateHelpTicketStatus.OPEN);
|
||||
}
|
||||
|
||||
static async closePrivateHelpTicket(threadId: string) {
|
||||
await Database.db.run('UPDATE private_help_tickets SET status = ? WHERE thread_id = ?', PrivateHelpTicketStatus.CLOSED, threadId);
|
||||
}
|
||||
|
||||
static async getLatestPrivateHelpTicketBy(userId: string): Promise<PrivateHelpTicket | undefined> {
|
||||
return await Database.db.get<PrivateHelpTicket>('SELECT * from private_help_tickets WHERE user_id = ? ORDER BY timestamp DESC LIMIT 1', userId);
|
||||
}
|
||||
|
||||
static async getAllOpenPrivateHelpTickets(): Promise<PrivateHelpTicket[]> {
|
||||
return await Database.db.all<PrivateHelpTicket[]>('SELECT * from private_help_tickets WHERE status = ?', PrivateHelpTicketStatus.OPEN);
|
||||
}
|
||||
|
||||
// -- END PRIVATE HELP TICKETS
|
||||
}
|
||||
+49
-49
@@ -1,50 +1,50 @@
|
||||
import { createClient, SocketClosedUnexpectedlyError } from '@redis/client';
|
||||
import { Client } from 'discord.js';
|
||||
import { banUpdateHandler, ticketUpdateHandler } from '../utils';
|
||||
|
||||
let discordClient: Client;
|
||||
|
||||
export async function openRedisClient(url: string, discClient: Client) {
|
||||
const client = await createClient({
|
||||
url: `redis://${url}`,
|
||||
socket: {
|
||||
reconnectStrategy: 60000
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
if (error.code == 'ECONNREFUSED') {
|
||||
console.error("Couldn't connect to redis database: Connection refused (is redis on? is the port reachable?)");
|
||||
} else if (error instanceof SocketClosedUnexpectedlyError) {
|
||||
console.error('Redis server closed unexpectedly. Attempting reconnect every 60 seconds.');
|
||||
} else {
|
||||
console.error('Redis error:');
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('Connected to redis database');
|
||||
});
|
||||
|
||||
client.on('reconnecting', () => {
|
||||
console.log('Attempting to reconnect to redis database');
|
||||
});
|
||||
|
||||
client.once('connect', () => {
|
||||
client.subscribe(['ticket_updates', 'ban_updates'], updateHandler);
|
||||
});
|
||||
|
||||
client.connect();
|
||||
|
||||
discordClient = discClient;
|
||||
}
|
||||
|
||||
function updateHandler(data: string, channel: string) {
|
||||
switch (channel) {
|
||||
case 'ticket_updates':
|
||||
return ticketUpdateHandler(discordClient, data);
|
||||
case 'ban_updates':
|
||||
return banUpdateHandler(discordClient, data);
|
||||
}
|
||||
import { createClient, SocketClosedUnexpectedlyError } from '@redis/client';
|
||||
import { Client } from 'discord.js';
|
||||
import { banUpdateHandler, ticketUpdateHandler } from '../utils';
|
||||
|
||||
let discordClient: Client;
|
||||
|
||||
export async function openRedisClient(url: string, discClient: Client) {
|
||||
const client = await createClient({
|
||||
url: `redis://${url}`,
|
||||
socket: {
|
||||
reconnectStrategy: 60000
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
if (error.code == 'ECONNREFUSED') {
|
||||
console.error("Couldn't connect to redis database: Connection refused (is redis on? is the port reachable?)");
|
||||
} else if (error instanceof SocketClosedUnexpectedlyError) {
|
||||
console.error('Redis server closed unexpectedly. Attempting reconnect every 60 seconds.');
|
||||
} else {
|
||||
console.error('Redis error:');
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('Connected to redis database');
|
||||
});
|
||||
|
||||
client.on('reconnecting', () => {
|
||||
console.log('Attempting to reconnect to redis database');
|
||||
});
|
||||
|
||||
client.once('connect', () => {
|
||||
client.subscribe(['ticket_updates', 'ban_updates'], updateHandler);
|
||||
});
|
||||
|
||||
client.connect();
|
||||
|
||||
discordClient = discClient;
|
||||
}
|
||||
|
||||
function updateHandler(data: string, channel: string) {
|
||||
switch (channel) {
|
||||
case 'ticket_updates':
|
||||
return ticketUpdateHandler(discordClient, data);
|
||||
case 'ban_updates':
|
||||
return banUpdateHandler(discordClient, data);
|
||||
}
|
||||
}
|
||||
Vendored
+8
-8
@@ -1,9 +1,9 @@
|
||||
import { Client, ContextMenuCommandBuilder, SlashCommandBuilder } from 'discord.js';
|
||||
import { Handler } from './handler';
|
||||
|
||||
export type CommandBuilder = ContextMenuCommandBuilder | SlashCommandBuilder;
|
||||
|
||||
export interface Command extends Handler {
|
||||
data: CommandBuilder | ((client: Client) => Promise<CommandBuilder>);
|
||||
guilds?: string[];
|
||||
import { Client, ContextMenuCommandBuilder, SlashCommandBuilder } from 'discord.js';
|
||||
import { Handler } from './handler';
|
||||
|
||||
export type CommandBuilder = ContextMenuCommandBuilder | SlashCommandBuilder;
|
||||
|
||||
export interface Command extends Handler {
|
||||
data: CommandBuilder | ((client: Client) => Promise<CommandBuilder>);
|
||||
guilds?: string[];
|
||||
}
|
||||
Vendored
+78
-78
@@ -1,79 +1,79 @@
|
||||
export type LoggedMessage = {
|
||||
id: string
|
||||
author_id: string
|
||||
author_name: string
|
||||
channel_id: string
|
||||
attachments: string
|
||||
stickers: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type GuildSettings = {
|
||||
guild_id: string
|
||||
general_chat_id?: string
|
||||
new_member_channel_id?: string
|
||||
tickets_channel_id?: string
|
||||
event_logs_channel_id?: string
|
||||
discord_logs_channel_id?: string
|
||||
audit_logs_channel_id?: string
|
||||
voice_logs_channel_id?: string
|
||||
admin_role_id?: string
|
||||
private_help_role_id?: string
|
||||
devwatch_role_id?: string
|
||||
staff_categories?: string
|
||||
safe_channels?: string
|
||||
link_skip_channels?: string
|
||||
github_release_channel?: string
|
||||
moderator_channel_id?: string
|
||||
private_help_channel_id?: string
|
||||
}
|
||||
|
||||
export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels';
|
||||
|
||||
export type TicketMessage = {
|
||||
id: number
|
||||
message_id: string
|
||||
}
|
||||
|
||||
export type TicketPhrase = {
|
||||
id: number
|
||||
user_id: string
|
||||
phrase: string
|
||||
}
|
||||
|
||||
export type Note = {
|
||||
id: number
|
||||
user_id: string
|
||||
reason: string
|
||||
mod_id: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export type Ban = {
|
||||
id: number
|
||||
user_id: string
|
||||
expires: 0 | 1
|
||||
expires_at: string
|
||||
full_ban: 0 | 1
|
||||
}
|
||||
|
||||
export type GithubUserMapping = {
|
||||
id: number
|
||||
discord_id: string
|
||||
github_username: string
|
||||
}
|
||||
|
||||
export type KnowledgebaseItem = {
|
||||
id: number
|
||||
guild_id: string
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PrivateHelpTicket = {
|
||||
id: number
|
||||
user_id: string
|
||||
thread_id: string
|
||||
status: PrivateHelpTicketStatus
|
||||
timestamp: string
|
||||
export type LoggedMessage = {
|
||||
id: string
|
||||
author_id: string
|
||||
author_name: string
|
||||
channel_id: string
|
||||
attachments: string
|
||||
stickers: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type GuildSettings = {
|
||||
guild_id: string
|
||||
general_chat_id?: string
|
||||
new_member_channel_id?: string
|
||||
tickets_channel_id?: string
|
||||
event_logs_channel_id?: string
|
||||
discord_logs_channel_id?: string
|
||||
audit_logs_channel_id?: string
|
||||
voice_logs_channel_id?: string
|
||||
admin_role_id?: string
|
||||
private_help_role_id?: string
|
||||
devwatch_role_id?: string
|
||||
staff_categories?: string
|
||||
safe_channels?: string
|
||||
link_skip_channels?: string
|
||||
github_release_channel?: string
|
||||
moderator_channel_id?: string
|
||||
private_help_channel_id?: string
|
||||
}
|
||||
|
||||
export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels';
|
||||
|
||||
export type TicketMessage = {
|
||||
id: number
|
||||
message_id: string
|
||||
}
|
||||
|
||||
export type TicketPhrase = {
|
||||
id: number
|
||||
user_id: string
|
||||
phrase: string
|
||||
}
|
||||
|
||||
export type Note = {
|
||||
id: number
|
||||
user_id: string
|
||||
reason: string
|
||||
mod_id: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export type Ban = {
|
||||
id: number
|
||||
user_id: string
|
||||
expires: 0 | 1
|
||||
expires_at: string
|
||||
full_ban: 0 | 1
|
||||
}
|
||||
|
||||
export type GithubUserMapping = {
|
||||
id: number
|
||||
discord_id: string
|
||||
github_username: string
|
||||
}
|
||||
|
||||
export type KnowledgebaseItem = {
|
||||
id: number
|
||||
guild_id: string
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PrivateHelpTicket = {
|
||||
id: number
|
||||
user_id: string
|
||||
thread_id: string
|
||||
status: PrivateHelpTicketStatus
|
||||
timestamp: string
|
||||
}
|
||||
Vendored
+156
-156
@@ -1,157 +1,157 @@
|
||||
export type E621User = {
|
||||
wiki_page_version_count: number
|
||||
artist_version_count: number
|
||||
pool_version_count: number
|
||||
forum_post_count: number
|
||||
comment_count: number
|
||||
flag_count: number
|
||||
favorite_count: number
|
||||
positive_feedback_count: number
|
||||
neutral_feedback_count: number
|
||||
negative_feedback_count: number
|
||||
upload_limit: number
|
||||
profile_about: string
|
||||
profile_artinfo: string
|
||||
id: number
|
||||
created_at: string
|
||||
name: string
|
||||
level: number
|
||||
base_upload_limit: number
|
||||
post_upload_count: number
|
||||
post_update_count: number
|
||||
note_update_count: number
|
||||
is_banned: boolean
|
||||
can_approve_posts: boolean
|
||||
can_upload_free: boolean
|
||||
level_string: string
|
||||
avatar_id: number
|
||||
}
|
||||
|
||||
export type E621Post = {
|
||||
id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
file: E621File
|
||||
preview: E621PreviewFile
|
||||
sample: E621SampleFile
|
||||
score: E621ScoreData
|
||||
tags: E621Tags
|
||||
locked_tags: string[]
|
||||
change_seq: number
|
||||
flags: E621FlagData
|
||||
rating: 's' | 'q' | 'e'
|
||||
fav_count: number
|
||||
sources: string[]
|
||||
pools: number[]
|
||||
relationships: E621PostRelationships
|
||||
approver_id: number
|
||||
uploader_id: number
|
||||
description: string
|
||||
comment_count: number
|
||||
is_favorited: boolean
|
||||
has_notes: boolean
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
export type E621File = {
|
||||
width: number
|
||||
height: number
|
||||
ext: 'png' | 'jpg' | 'mp4' | 'webm'
|
||||
size: number
|
||||
md5: string
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type E621PreviewFile = {
|
||||
width: number
|
||||
height: number
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type E621SampleFile = {
|
||||
has: boolean
|
||||
height: number
|
||||
width: number
|
||||
url: string | null
|
||||
// Typing this will be a pain in the ass, so I skipped it for now.
|
||||
alternates: any
|
||||
}
|
||||
|
||||
export type E621ScoreData = {
|
||||
up: number
|
||||
down: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type E621Tags = {
|
||||
general: string[]
|
||||
artist: string[]
|
||||
contributor: string[]
|
||||
copyright: string[]
|
||||
character: string[]
|
||||
species: string[]
|
||||
invalid: string[]
|
||||
meta: string[]
|
||||
lore: string[]
|
||||
}
|
||||
|
||||
export type E621FlagData = {
|
||||
pending: boolean
|
||||
flagged: boolean
|
||||
note_locked: boolean
|
||||
status_locked: boolean
|
||||
rating_locked: boolean
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export type E621PostRelationships = {
|
||||
parent_id: number | null
|
||||
has_children: boolean
|
||||
has_active_children: boolean
|
||||
children: number[]
|
||||
}
|
||||
|
||||
export type Ticket = {
|
||||
id: number
|
||||
user_id: number
|
||||
user: string
|
||||
claimant: string | null
|
||||
target?: string
|
||||
accused_id?: number
|
||||
target_id: number
|
||||
status: 'pending' | 'partial' | 'approved'
|
||||
category: 'blip' | 'comment' | 'dmail' | 'forum' | 'pool' | 'post' | 'set' | 'user' | 'wiki'
|
||||
reason: string
|
||||
};
|
||||
|
||||
export type TicketUpdate = {
|
||||
action: 'claim' | 'create' | 'unclaim' | 'update'
|
||||
ticket: Ticket
|
||||
};
|
||||
|
||||
export type Ban = {
|
||||
id: number
|
||||
user_id: number
|
||||
banner_id: number
|
||||
expires_at: string
|
||||
reason: string
|
||||
};
|
||||
|
||||
export type BanUpdate = {
|
||||
action: 'create' | 'update' | 'delete'
|
||||
ban: Ban
|
||||
};
|
||||
|
||||
export type RecordCategory = 'positive' | 'negative' | 'neutral'
|
||||
|
||||
export type Record = {
|
||||
id: number
|
||||
user_id: number
|
||||
creator_id: number
|
||||
created_at: string
|
||||
body: string
|
||||
category: RecordCategory
|
||||
updated_at: string
|
||||
updater_id: number
|
||||
is_deleted: boolean
|
||||
export type E621User = {
|
||||
wiki_page_version_count: number
|
||||
artist_version_count: number
|
||||
pool_version_count: number
|
||||
forum_post_count: number
|
||||
comment_count: number
|
||||
flag_count: number
|
||||
favorite_count: number
|
||||
positive_feedback_count: number
|
||||
neutral_feedback_count: number
|
||||
negative_feedback_count: number
|
||||
upload_limit: number
|
||||
profile_about: string
|
||||
profile_artinfo: string
|
||||
id: number
|
||||
created_at: string
|
||||
name: string
|
||||
level: number
|
||||
base_upload_limit: number
|
||||
post_upload_count: number
|
||||
post_update_count: number
|
||||
note_update_count: number
|
||||
is_banned: boolean
|
||||
can_approve_posts: boolean
|
||||
can_upload_free: boolean
|
||||
level_string: string
|
||||
avatar_id: number
|
||||
}
|
||||
|
||||
export type E621Post = {
|
||||
id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
file: E621File
|
||||
preview: E621PreviewFile
|
||||
sample: E621SampleFile
|
||||
score: E621ScoreData
|
||||
tags: E621Tags
|
||||
locked_tags: string[]
|
||||
change_seq: number
|
||||
flags: E621FlagData
|
||||
rating: 's' | 'q' | 'e'
|
||||
fav_count: number
|
||||
sources: string[]
|
||||
pools: number[]
|
||||
relationships: E621PostRelationships
|
||||
approver_id: number
|
||||
uploader_id: number
|
||||
description: string
|
||||
comment_count: number
|
||||
is_favorited: boolean
|
||||
has_notes: boolean
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
export type E621File = {
|
||||
width: number
|
||||
height: number
|
||||
ext: 'png' | 'jpg' | 'mp4' | 'webm'
|
||||
size: number
|
||||
md5: string
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type E621PreviewFile = {
|
||||
width: number
|
||||
height: number
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type E621SampleFile = {
|
||||
has: boolean
|
||||
height: number
|
||||
width: number
|
||||
url: string | null
|
||||
// Typing this will be a pain in the ass, so I skipped it for now.
|
||||
alternates: any
|
||||
}
|
||||
|
||||
export type E621ScoreData = {
|
||||
up: number
|
||||
down: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type E621Tags = {
|
||||
general: string[]
|
||||
artist: string[]
|
||||
contributor: string[]
|
||||
copyright: string[]
|
||||
character: string[]
|
||||
species: string[]
|
||||
invalid: string[]
|
||||
meta: string[]
|
||||
lore: string[]
|
||||
}
|
||||
|
||||
export type E621FlagData = {
|
||||
pending: boolean
|
||||
flagged: boolean
|
||||
note_locked: boolean
|
||||
status_locked: boolean
|
||||
rating_locked: boolean
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export type E621PostRelationships = {
|
||||
parent_id: number | null
|
||||
has_children: boolean
|
||||
has_active_children: boolean
|
||||
children: number[]
|
||||
}
|
||||
|
||||
export type Ticket = {
|
||||
id: number
|
||||
user_id: number
|
||||
user: string
|
||||
claimant: string | null
|
||||
target?: string
|
||||
accused_id?: number
|
||||
target_id: number
|
||||
status: 'pending' | 'partial' | 'approved'
|
||||
category: 'blip' | 'comment' | 'dmail' | 'forum' | 'pool' | 'post' | 'set' | 'user' | 'wiki'
|
||||
reason: string
|
||||
};
|
||||
|
||||
export type TicketUpdate = {
|
||||
action: 'claim' | 'create' | 'unclaim' | 'update'
|
||||
ticket: Ticket
|
||||
};
|
||||
|
||||
export type Ban = {
|
||||
id: number
|
||||
user_id: number
|
||||
banner_id: number
|
||||
expires_at: string
|
||||
reason: string
|
||||
};
|
||||
|
||||
export type BanUpdate = {
|
||||
action: 'create' | 'update' | 'delete'
|
||||
ban: Ban
|
||||
};
|
||||
|
||||
export type RecordCategory = 'positive' | 'negative' | 'neutral'
|
||||
|
||||
export type Record = {
|
||||
id: number
|
||||
user_id: number
|
||||
creator_id: number
|
||||
created_at: string
|
||||
body: string
|
||||
category: RecordCategory
|
||||
updated_at: string
|
||||
updater_id: number
|
||||
is_deleted: boolean
|
||||
}
|
||||
Vendored
+9
-9
@@ -1,10 +1,10 @@
|
||||
import { Client, Interaction } from 'discord.js';
|
||||
|
||||
type HandlerFunction = (client: Client, interaction: Interaction, ...args: any) => Promise<void>
|
||||
|
||||
export interface Handler {
|
||||
name: string;
|
||||
handler: HandlerFunction;
|
||||
init?: (client: Client) => Promise<void>;
|
||||
autoComplete?: HandlerFunction;
|
||||
import { Client, Interaction } from 'discord.js';
|
||||
|
||||
type HandlerFunction = (client: Client, interaction: Interaction, ...args: any) => Promise<void>
|
||||
|
||||
export interface Handler {
|
||||
name: string;
|
||||
handler: HandlerFunction;
|
||||
init?: (client: Client) => Promise<void>;
|
||||
autoComplete?: HandlerFunction;
|
||||
}
|
||||
Vendored
+37
-37
@@ -1,38 +1,38 @@
|
||||
import { ActionRowBuilder, APIRole, ButtonBuilder, EmbedBuilder, PermissionsBitField, TextChannel } from 'discord.js';
|
||||
|
||||
export type MessageContent = { content?: string, embeds?: EmbedBuilder[], components: ActionRowBuilder<ButtonBuilder>[] };
|
||||
|
||||
export type RoleChangeLog = {
|
||||
key: '$add' | '$remove',
|
||||
old?: Pick<APIRole, 'id' | 'name'>[],
|
||||
new?: Pick<APIRole, 'id' | 'name'>[]
|
||||
}
|
||||
|
||||
type ApplicationCommandPermission = {
|
||||
type: 1 | 2 | 3,
|
||||
permission: boolean,
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ApplicationCommandPermissionChangeLog = {
|
||||
key: sting,
|
||||
old?: ApplicationCommandPermission,
|
||||
new?: ApplicationCommandPermission
|
||||
}
|
||||
|
||||
export type TimeoutChangeLog = {
|
||||
key: 'communication_disabled_until',
|
||||
old?: string,
|
||||
new?: string
|
||||
}
|
||||
|
||||
export type PermissionsChangeLog = {
|
||||
key: 'permissions' | 'allow' | 'deny',
|
||||
old?: number,
|
||||
new?: number
|
||||
}
|
||||
|
||||
export type PinExtras = {
|
||||
channel: TextChannel,
|
||||
messageId: string
|
||||
import { ActionRowBuilder, APIRole, ButtonBuilder, EmbedBuilder, PermissionsBitField, TextChannel } from 'discord.js';
|
||||
|
||||
export type MessageContent = { content?: string, embeds?: EmbedBuilder[], components: ActionRowBuilder<ButtonBuilder>[] };
|
||||
|
||||
export type RoleChangeLog = {
|
||||
key: '$add' | '$remove',
|
||||
old?: Pick<APIRole, 'id' | 'name'>[],
|
||||
new?: Pick<APIRole, 'id' | 'name'>[]
|
||||
}
|
||||
|
||||
type ApplicationCommandPermission = {
|
||||
type: 1 | 2 | 3,
|
||||
permission: boolean,
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ApplicationCommandPermissionChangeLog = {
|
||||
key: sting,
|
||||
old?: ApplicationCommandPermission,
|
||||
new?: ApplicationCommandPermission
|
||||
}
|
||||
|
||||
export type TimeoutChangeLog = {
|
||||
key: 'communication_disabled_until',
|
||||
old?: string,
|
||||
new?: string
|
||||
}
|
||||
|
||||
export type PermissionsChangeLog = {
|
||||
key: 'permissions' | 'allow' | 'deny',
|
||||
old?: number,
|
||||
new?: number
|
||||
}
|
||||
|
||||
export type PinExtras = {
|
||||
channel: TextChannel,
|
||||
messageId: string
|
||||
}
|
||||
Vendored
+1
-1
@@ -3,4 +3,4 @@ export * from './database-types.d';
|
||||
export * from './e621-types.d';
|
||||
export * from './handler.d';
|
||||
export * from './helper-types.d';
|
||||
export * from './scheduler.d';
|
||||
export * from './scheduler.d';
|
||||
|
||||
+105
-105
@@ -1,106 +1,106 @@
|
||||
import { Guild } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { userIsBanned } from './e621-utils';
|
||||
import { config } from '../config';
|
||||
|
||||
export type AltData = {
|
||||
type: 'e621' | 'discord'
|
||||
thisId: number | string
|
||||
banned: boolean
|
||||
alts: AltData[]
|
||||
};
|
||||
|
||||
|
||||
export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
|
||||
const e621UserIds = await Database.getE621Ids(discordId);
|
||||
|
||||
const toIgnore = ignore.concat(e621UserIds);
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const e621Id of e621UserIds) {
|
||||
if (ignore.includes(e621Id)) continue;
|
||||
|
||||
const alts = await getDiscordAlts(e621Id, guild, depth + 1, toIgnore);
|
||||
|
||||
const banned = await userIsBanned(e621Id);
|
||||
|
||||
content += `${' '.repeat((depth - 1) * 2)}- ${config.E621_BASE_URL}/users/${e621Id}${banned ? ' [BANNED]' : ''}\n${alts}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function getDiscordAlts(e621Id: number, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
|
||||
const discordIds = await Database.getDiscordIds(e621Id);
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const discordId of discordIds) {
|
||||
const alts = await getE621Alts(discordId, guild, depth + 1, ignore);
|
||||
|
||||
let banned = false;
|
||||
|
||||
// It's either this or fetch all the bans and sift through them for every discord alt.
|
||||
try {
|
||||
banned = !!(await guild.bans.fetch(discordId));
|
||||
} catch (e) { }
|
||||
|
||||
content += `${' '.repeat((depth - 1) * 2)}- <@${discordId}> (${discordId})${banned ? ' [BANNED]' : ''}\n${alts}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function comprehensiveAltLookupFromDiscord(discordId: string, guild: Guild | null): Promise<AltData> {
|
||||
return getE621AltData(discordId, guild);
|
||||
}
|
||||
|
||||
export async function comprehensiveAltLookupFromE621(e621Id: number, guild: Guild | null): Promise<AltData> {
|
||||
return getDiscordAltData(e621Id, guild);
|
||||
}
|
||||
|
||||
async function getE621AltData(discordId: string, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> {
|
||||
const e621UserIds = await Database.getE621Ids(discordId);
|
||||
|
||||
const toIgnore = ignore.concat(e621UserIds);
|
||||
|
||||
let banned = false;
|
||||
|
||||
// It's either this or fetch all the bans and sift through them for every discord alt.
|
||||
try {
|
||||
banned = guild ? !!(await guild.bans.fetch(discordId)) : false;
|
||||
} catch (e) { }
|
||||
|
||||
const data: AltData = { type: 'discord', thisId: discordId, banned, alts: [] };
|
||||
|
||||
for (const e621Id of e621UserIds) {
|
||||
if (ignore.includes(e621Id)) continue;
|
||||
|
||||
data.alts.push(await getDiscordAltData(e621Id, guild, depth + 1, toIgnore));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> {
|
||||
const discordIds = await Database.getDiscordIds(e621Id);
|
||||
|
||||
const data: AltData = { type: 'e621', thisId: e621Id, banned: await userIsBanned(e621Id), alts: [] };
|
||||
|
||||
for (const discordId of discordIds) {
|
||||
data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function e621IdsFromAltData(altData: AltData, data: number[] = []) {
|
||||
if (altData.type == 'e621' && !data.includes(altData.thisId as number)) data.push(altData.thisId as number);
|
||||
|
||||
for (const alt of altData.alts) {
|
||||
e621IdsFromAltData(alt, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
import { Guild } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { userIsBanned } from './e621-utils';
|
||||
import { config } from '../config';
|
||||
|
||||
export type AltData = {
|
||||
type: 'e621' | 'discord'
|
||||
thisId: number | string
|
||||
banned: boolean
|
||||
alts: AltData[]
|
||||
};
|
||||
|
||||
|
||||
export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
|
||||
const e621UserIds = await Database.getE621Ids(discordId);
|
||||
|
||||
const toIgnore = ignore.concat(e621UserIds);
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const e621Id of e621UserIds) {
|
||||
if (ignore.includes(e621Id)) continue;
|
||||
|
||||
const alts = await getDiscordAlts(e621Id, guild, depth + 1, toIgnore);
|
||||
|
||||
const banned = await userIsBanned(e621Id);
|
||||
|
||||
content += `${' '.repeat((depth - 1) * 2)}- ${config.E621_BASE_URL}/users/${e621Id}${banned ? ' [BANNED]' : ''}\n${alts}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function getDiscordAlts(e621Id: number, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
|
||||
const discordIds = await Database.getDiscordIds(e621Id);
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const discordId of discordIds) {
|
||||
const alts = await getE621Alts(discordId, guild, depth + 1, ignore);
|
||||
|
||||
let banned = false;
|
||||
|
||||
// It's either this or fetch all the bans and sift through them for every discord alt.
|
||||
try {
|
||||
banned = !!(await guild.bans.fetch(discordId));
|
||||
} catch (e) { }
|
||||
|
||||
content += `${' '.repeat((depth - 1) * 2)}- <@${discordId}> (${discordId})${banned ? ' [BANNED]' : ''}\n${alts}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function comprehensiveAltLookupFromDiscord(discordId: string, guild: Guild | null): Promise<AltData> {
|
||||
return getE621AltData(discordId, guild);
|
||||
}
|
||||
|
||||
export async function comprehensiveAltLookupFromE621(e621Id: number, guild: Guild | null): Promise<AltData> {
|
||||
return getDiscordAltData(e621Id, guild);
|
||||
}
|
||||
|
||||
async function getE621AltData(discordId: string, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> {
|
||||
const e621UserIds = await Database.getE621Ids(discordId);
|
||||
|
||||
const toIgnore = ignore.concat(e621UserIds);
|
||||
|
||||
let banned = false;
|
||||
|
||||
// It's either this or fetch all the bans and sift through them for every discord alt.
|
||||
try {
|
||||
banned = guild ? !!(await guild.bans.fetch(discordId)) : false;
|
||||
} catch (e) { }
|
||||
|
||||
const data: AltData = { type: 'discord', thisId: discordId, banned, alts: [] };
|
||||
|
||||
for (const e621Id of e621UserIds) {
|
||||
if (ignore.includes(e621Id)) continue;
|
||||
|
||||
data.alts.push(await getDiscordAltData(e621Id, guild, depth + 1, toIgnore));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, ignore: number[] = []): Promise<AltData> {
|
||||
const discordIds = await Database.getDiscordIds(e621Id);
|
||||
|
||||
const data: AltData = { type: 'e621', thisId: e621Id, banned: await userIsBanned(e621Id), alts: [] };
|
||||
|
||||
for (const discordId of discordIds) {
|
||||
data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function e621IdsFromAltData(altData: AltData, data: number[] = []) {
|
||||
if (altData.type == 'e621' && !data.includes(altData.thisId as number)) data.push(altData.thisId as number);
|
||||
|
||||
for (const alt of altData.alts) {
|
||||
e621IdsFromAltData(alt, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export function getArrayDifference(oldArr: any[], newArr: any[]) {
|
||||
const added = newArr.filter(e => !oldArr.includes(e));
|
||||
const removed = oldArr.filter(e => !newArr.includes(e));
|
||||
|
||||
return { added, removed };
|
||||
export function getArrayDifference(oldArr: any[], newArr: any[]) {
|
||||
const added = newArr.filter(e => !oldArr.includes(e));
|
||||
const removed = oldArr.filter(e => !newArr.includes(e));
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
+196
-196
@@ -1,197 +1,197 @@
|
||||
import { AuditLogChange, AuditLogEvent, Guild, GuildAuditLogsEntry, PermissionsBitField, PermissionsString, time, TimestampStyles } from 'discord.js';
|
||||
import { ApplicationCommandPermissionChangeLog, PermissionsChangeLog, PinExtras, RoleChangeLog, TimeoutChangeLog } from '../types';
|
||||
import { getArrayDifference } from './array-utils';
|
||||
|
||||
export const enum TargetType {
|
||||
Unknown = 0,
|
||||
Role = 1,
|
||||
User = 2,
|
||||
Channel = 3
|
||||
};
|
||||
|
||||
const TARGETS_ROLES: AuditLogEvent[] = [
|
||||
AuditLogEvent.RoleCreate,
|
||||
AuditLogEvent.RoleDelete,
|
||||
AuditLogEvent.RoleUpdate
|
||||
];
|
||||
|
||||
const TARGETS_USERS: AuditLogEvent[] = [
|
||||
AuditLogEvent.MemberUpdate,
|
||||
AuditLogEvent.MemberKick,
|
||||
AuditLogEvent.MemberBanAdd,
|
||||
AuditLogEvent.MemberBanRemove,
|
||||
AuditLogEvent.MemberRoleUpdate,
|
||||
AuditLogEvent.MessageDelete,
|
||||
AuditLogEvent.MessagePin,
|
||||
AuditLogEvent.MessageUnpin
|
||||
];
|
||||
|
||||
const TARGETS_CHANNELS: AuditLogEvent[] = [
|
||||
AuditLogEvent.ChannelCreate,
|
||||
AuditLogEvent.ChannelUpdate,
|
||||
AuditLogEvent.ChannelDelete,
|
||||
AuditLogEvent.ThreadCreate,
|
||||
AuditLogEvent.ThreadUpdate,
|
||||
AuditLogEvent.ThreadDelete,
|
||||
AuditLogEvent.ChannelOverwriteCreate,
|
||||
AuditLogEvent.ChannelOverwriteDelete,
|
||||
AuditLogEvent.ChannelOverwriteUpdate
|
||||
];
|
||||
|
||||
export function getTargetType(actionType: AuditLogEvent): TargetType {
|
||||
if (TARGETS_ROLES.includes(actionType)) return TargetType.Role;
|
||||
else if (TARGETS_USERS.includes(actionType)) return TargetType.User;
|
||||
else if (TARGETS_CHANNELS.includes(actionType)) return TargetType.Channel;
|
||||
|
||||
return TargetType.Unknown;
|
||||
}
|
||||
|
||||
export function formatSnowflake(snowflake: string, targetType: TargetType): string {
|
||||
if (targetType == TargetType.Role) return `<@&${snowflake}>`;
|
||||
else if (targetType == TargetType.User) return `<@${snowflake}>`;
|
||||
else if (targetType == TargetType.Channel) return `<#${snowflake}>`;
|
||||
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
export function formatChanges(entry: GuildAuditLogsEntry): string {
|
||||
return entry.changes.map(c => formatChange(c, entry)).filter(e => e).join('\n');
|
||||
}
|
||||
|
||||
export function formatExtras(entry: GuildAuditLogsEntry, guild: Guild): string {
|
||||
if (entry.action == AuditLogEvent.MessagePin || entry.action == AuditLogEvent.MessageUnpin)
|
||||
return formatMessagePin(entry.extra as unknown as PinExtras, guild);
|
||||
|
||||
if (entry.action == AuditLogEvent.ChannelOverwriteCreate
|
||||
|| entry.action == AuditLogEvent.ChannelOverwriteDelete
|
||||
|| entry.action == AuditLogEvent.ChannelOverwriteUpdate) {
|
||||
return `Target: ${entry.extra!.toString()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const reserialized = JSON.parse(JSON.stringify(entry));
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(reserialized.extra ?? {})) {
|
||||
if (!value) continue;
|
||||
|
||||
if (key == 'channel_id' || key == 'channel') {
|
||||
results.push(`channel: ${formatSnowflake(value as string, TargetType.Channel)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
return results.join('\n');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatChange(change: AuditLogChange, entry: GuildAuditLogsEntry): string | undefined {
|
||||
if (entry.action == AuditLogEvent.ApplicationCommandPermissionUpdate) {
|
||||
return formatApplicationPermissionsUpdate(change as ApplicationCommandPermissionChangeLog);
|
||||
}
|
||||
|
||||
switch (change.key) {
|
||||
case '$add':
|
||||
case '$remove':
|
||||
return formatMemberRoleChange(change);
|
||||
|
||||
case 'communication_disabled_until':
|
||||
return formatTimeoutChange(change);
|
||||
|
||||
case 'permissions':
|
||||
case 'allow':
|
||||
case 'deny':
|
||||
return formatPermissionOrOverwrites(change as PermissionsChangeLog);
|
||||
}
|
||||
|
||||
const oldValue = change.key == 'nick' ? `\`${change.old}\`` : change.old;
|
||||
const newValue = change.key == 'nick' ? `\`${change.new}\`` : change.new;
|
||||
|
||||
if (change.new !== undefined && change.old === undefined)
|
||||
return `Set ${change.key} to ${newValue}`;
|
||||
|
||||
if (change.new === undefined && change.old !== undefined)
|
||||
return `Set ${change.key} with value ${oldValue} to default/null`;
|
||||
|
||||
return `Set ${change.key} from ${oldValue} to ${newValue}`;
|
||||
}
|
||||
|
||||
function formatApplicationPermissionsUpdate(change: ApplicationCommandPermissionChangeLog): string | undefined {
|
||||
if (change.new !== undefined && change.old === undefined)
|
||||
return `${change.new.permission ? 'Allowed' : 'Denied'} access ${change.new.type == 3 ? 'in' : (change.new.permission ? 'to' : 'from')} ${formatSnowflake(change.new.id, change.new.type)} for command: ${change.key}`;
|
||||
|
||||
if (change.new === undefined && change.old !== undefined)
|
||||
return `Removed permission overrides from ${formatSnowflake(change.old.id, change.old.type)} for command: ${change.key}`;
|
||||
|
||||
return `Updated permission overrides ${change.new!.type == 3 ? 'in' : 'for'} ${formatSnowflake(change.new!.id, change.new!.type)}: ${change.new!.permission ? 'allowed access to' : 'revoked access to'} command: ${change.key}`;
|
||||
}
|
||||
|
||||
function formatMemberRoleChange(change: RoleChangeLog): string | undefined {
|
||||
if (!change.new) return;
|
||||
|
||||
const changes: string[] = [];
|
||||
|
||||
for (const roleChange of change.new) {
|
||||
if (change.key == '$add') changes.push(`Added role ${formatSnowflake(roleChange.id, TargetType.Role)}`);
|
||||
else changes.push(`Removed role ${formatSnowflake(roleChange.id, TargetType.Role)}`);
|
||||
}
|
||||
|
||||
if (changes.length == 0) return;
|
||||
|
||||
return changes.join('\n');
|
||||
}
|
||||
|
||||
function formatTimeoutChange(change: TimeoutChangeLog): string {
|
||||
if (!change.new) return 'Timeout removed';
|
||||
|
||||
const date = new Date(change.new);
|
||||
|
||||
return `Timeout until ${time(date, TimestampStyles.RelativeTime)}`;
|
||||
}
|
||||
|
||||
function formatPermissionOrOverwrites(change: PermissionsChangeLog): string {
|
||||
const oldPerms = new PermissionsBitField(BigInt(change.old ?? 0)).toArray();
|
||||
const newPerms = new PermissionsBitField(BigInt(change.new ?? 0)).toArray();
|
||||
|
||||
switch (change.key) {
|
||||
case 'permissions':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Removed permission(s)', 'Added permission(s)');
|
||||
|
||||
case 'allow':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Allow removed', 'Allow added');
|
||||
|
||||
case 'deny':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Deny removed', 'Deny added');
|
||||
|
||||
default:
|
||||
return formatPermissionChange(oldPerms, newPerms, `${change.key} removed`, `${change.key} added`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatPermissionChange(oldPermissions: PermissionsString[], newPermissions: PermissionsString[], removedDescription: string, addedDescription: string) {
|
||||
const { added, removed } = getArrayDifference(oldPermissions, newPermissions);
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
if (added.length > 0) {
|
||||
result.push(`${addedDescription}: ${added.join(', ')}`);
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
result.push(`${removedDescription}: ${removed.join(', ')}`);
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
function formatMessagePin(data: PinExtras, guild: Guild): string {
|
||||
return `Message: [${data.messageId}](https://discord.com/channels/${guild.id}/${data.channel.id}/${data.messageId})`;
|
||||
import { AuditLogChange, AuditLogEvent, Guild, GuildAuditLogsEntry, PermissionsBitField, PermissionsString, time, TimestampStyles } from 'discord.js';
|
||||
import { ApplicationCommandPermissionChangeLog, PermissionsChangeLog, PinExtras, RoleChangeLog, TimeoutChangeLog } from '../types';
|
||||
import { getArrayDifference } from './array-utils';
|
||||
|
||||
export const enum TargetType {
|
||||
Unknown = 0,
|
||||
Role = 1,
|
||||
User = 2,
|
||||
Channel = 3
|
||||
};
|
||||
|
||||
const TARGETS_ROLES: AuditLogEvent[] = [
|
||||
AuditLogEvent.RoleCreate,
|
||||
AuditLogEvent.RoleDelete,
|
||||
AuditLogEvent.RoleUpdate
|
||||
];
|
||||
|
||||
const TARGETS_USERS: AuditLogEvent[] = [
|
||||
AuditLogEvent.MemberUpdate,
|
||||
AuditLogEvent.MemberKick,
|
||||
AuditLogEvent.MemberBanAdd,
|
||||
AuditLogEvent.MemberBanRemove,
|
||||
AuditLogEvent.MemberRoleUpdate,
|
||||
AuditLogEvent.MessageDelete,
|
||||
AuditLogEvent.MessagePin,
|
||||
AuditLogEvent.MessageUnpin
|
||||
];
|
||||
|
||||
const TARGETS_CHANNELS: AuditLogEvent[] = [
|
||||
AuditLogEvent.ChannelCreate,
|
||||
AuditLogEvent.ChannelUpdate,
|
||||
AuditLogEvent.ChannelDelete,
|
||||
AuditLogEvent.ThreadCreate,
|
||||
AuditLogEvent.ThreadUpdate,
|
||||
AuditLogEvent.ThreadDelete,
|
||||
AuditLogEvent.ChannelOverwriteCreate,
|
||||
AuditLogEvent.ChannelOverwriteDelete,
|
||||
AuditLogEvent.ChannelOverwriteUpdate
|
||||
];
|
||||
|
||||
export function getTargetType(actionType: AuditLogEvent): TargetType {
|
||||
if (TARGETS_ROLES.includes(actionType)) return TargetType.Role;
|
||||
else if (TARGETS_USERS.includes(actionType)) return TargetType.User;
|
||||
else if (TARGETS_CHANNELS.includes(actionType)) return TargetType.Channel;
|
||||
|
||||
return TargetType.Unknown;
|
||||
}
|
||||
|
||||
export function formatSnowflake(snowflake: string, targetType: TargetType): string {
|
||||
if (targetType == TargetType.Role) return `<@&${snowflake}>`;
|
||||
else if (targetType == TargetType.User) return `<@${snowflake}>`;
|
||||
else if (targetType == TargetType.Channel) return `<#${snowflake}>`;
|
||||
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
export function formatChanges(entry: GuildAuditLogsEntry): string {
|
||||
return entry.changes.map(c => formatChange(c, entry)).filter(e => e).join('\n');
|
||||
}
|
||||
|
||||
export function formatExtras(entry: GuildAuditLogsEntry, guild: Guild): string {
|
||||
if (entry.action == AuditLogEvent.MessagePin || entry.action == AuditLogEvent.MessageUnpin)
|
||||
return formatMessagePin(entry.extra as unknown as PinExtras, guild);
|
||||
|
||||
if (entry.action == AuditLogEvent.ChannelOverwriteCreate
|
||||
|| entry.action == AuditLogEvent.ChannelOverwriteDelete
|
||||
|| entry.action == AuditLogEvent.ChannelOverwriteUpdate) {
|
||||
return `Target: ${entry.extra!.toString()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const reserialized = JSON.parse(JSON.stringify(entry));
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(reserialized.extra ?? {})) {
|
||||
if (!value) continue;
|
||||
|
||||
if (key == 'channel_id' || key == 'channel') {
|
||||
results.push(`channel: ${formatSnowflake(value as string, TargetType.Channel)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
return results.join('\n');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatChange(change: AuditLogChange, entry: GuildAuditLogsEntry): string | undefined {
|
||||
if (entry.action == AuditLogEvent.ApplicationCommandPermissionUpdate) {
|
||||
return formatApplicationPermissionsUpdate(change as ApplicationCommandPermissionChangeLog);
|
||||
}
|
||||
|
||||
switch (change.key) {
|
||||
case '$add':
|
||||
case '$remove':
|
||||
return formatMemberRoleChange(change);
|
||||
|
||||
case 'communication_disabled_until':
|
||||
return formatTimeoutChange(change);
|
||||
|
||||
case 'permissions':
|
||||
case 'allow':
|
||||
case 'deny':
|
||||
return formatPermissionOrOverwrites(change as PermissionsChangeLog);
|
||||
}
|
||||
|
||||
const oldValue = change.key == 'nick' ? `\`${change.old}\`` : change.old;
|
||||
const newValue = change.key == 'nick' ? `\`${change.new}\`` : change.new;
|
||||
|
||||
if (change.new !== undefined && change.old === undefined)
|
||||
return `Set ${change.key} to ${newValue}`;
|
||||
|
||||
if (change.new === undefined && change.old !== undefined)
|
||||
return `Set ${change.key} with value ${oldValue} to default/null`;
|
||||
|
||||
return `Set ${change.key} from ${oldValue} to ${newValue}`;
|
||||
}
|
||||
|
||||
function formatApplicationPermissionsUpdate(change: ApplicationCommandPermissionChangeLog): string | undefined {
|
||||
if (change.new !== undefined && change.old === undefined)
|
||||
return `${change.new.permission ? 'Allowed' : 'Denied'} access ${change.new.type == 3 ? 'in' : (change.new.permission ? 'to' : 'from')} ${formatSnowflake(change.new.id, change.new.type)} for command: ${change.key}`;
|
||||
|
||||
if (change.new === undefined && change.old !== undefined)
|
||||
return `Removed permission overrides from ${formatSnowflake(change.old.id, change.old.type)} for command: ${change.key}`;
|
||||
|
||||
return `Updated permission overrides ${change.new!.type == 3 ? 'in' : 'for'} ${formatSnowflake(change.new!.id, change.new!.type)}: ${change.new!.permission ? 'allowed access to' : 'revoked access to'} command: ${change.key}`;
|
||||
}
|
||||
|
||||
function formatMemberRoleChange(change: RoleChangeLog): string | undefined {
|
||||
if (!change.new) return;
|
||||
|
||||
const changes: string[] = [];
|
||||
|
||||
for (const roleChange of change.new) {
|
||||
if (change.key == '$add') changes.push(`Added role ${formatSnowflake(roleChange.id, TargetType.Role)}`);
|
||||
else changes.push(`Removed role ${formatSnowflake(roleChange.id, TargetType.Role)}`);
|
||||
}
|
||||
|
||||
if (changes.length == 0) return;
|
||||
|
||||
return changes.join('\n');
|
||||
}
|
||||
|
||||
function formatTimeoutChange(change: TimeoutChangeLog): string {
|
||||
if (!change.new) return 'Timeout removed';
|
||||
|
||||
const date = new Date(change.new);
|
||||
|
||||
return `Timeout until ${time(date, TimestampStyles.RelativeTime)}`;
|
||||
}
|
||||
|
||||
function formatPermissionOrOverwrites(change: PermissionsChangeLog): string {
|
||||
const oldPerms = new PermissionsBitField(BigInt(change.old ?? 0)).toArray();
|
||||
const newPerms = new PermissionsBitField(BigInt(change.new ?? 0)).toArray();
|
||||
|
||||
switch (change.key) {
|
||||
case 'permissions':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Removed permission(s)', 'Added permission(s)');
|
||||
|
||||
case 'allow':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Allow removed', 'Allow added');
|
||||
|
||||
case 'deny':
|
||||
return formatPermissionChange(oldPerms, newPerms, 'Deny removed', 'Deny added');
|
||||
|
||||
default:
|
||||
return formatPermissionChange(oldPerms, newPerms, `${change.key} removed`, `${change.key} added`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatPermissionChange(oldPermissions: PermissionsString[], newPermissions: PermissionsString[], removedDescription: string, addedDescription: string) {
|
||||
const { added, removed } = getArrayDifference(oldPermissions, newPermissions);
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
if (added.length > 0) {
|
||||
result.push(`${addedDescription}: ${added.join(', ')}`);
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
result.push(`${removedDescription}: ${removed.join(', ')}`);
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
function formatMessagePin(data: PinExtras, guild: Guild): string {
|
||||
return `Message: [${data.messageId}](https://discord.com/channels/${guild.id}/${data.channel.id}/${data.messageId})`;
|
||||
}
|
||||
+48
-48
@@ -1,49 +1,49 @@
|
||||
import { Client } from 'discord.js';
|
||||
import { BanUpdate } from '../types';
|
||||
import { Database } from '../shared/Database';
|
||||
import { config } from '../config';
|
||||
|
||||
export async function banUpdateHandler(client: Client, update: string) {
|
||||
const data: BanUpdate = JSON.parse(update);
|
||||
|
||||
if (data.action == 'create') {
|
||||
kickDiscordAccounts(client, data);
|
||||
}
|
||||
// else if (data.action == 'delete') {
|
||||
// // unbanDiscordAccounts(data);
|
||||
// }
|
||||
}
|
||||
|
||||
async function kickDiscordAccounts(client: Client, data: BanUpdate) {
|
||||
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
for (const id of discordIds) {
|
||||
const member = await guild.members.fetch(id);
|
||||
|
||||
if (member) await member.kick(`Banned from e621 by: https://e621.net/users/${data.ban.banner_id}. Reason:\n${data.ban.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// async function banDiscordAccounts(data: BanUpdate) {
|
||||
// const guild = await discordClient.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
// const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
// for (const id of discordIds) {
|
||||
// await guild.bans.create(id, {
|
||||
// reason: data.ban.reason
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function unbanDiscordAccounts(data: BanUpdate) {
|
||||
// const guild = await discordClient.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
// const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
// for (const id of discordIds) {
|
||||
// await guild.bans.remove(id);
|
||||
// }
|
||||
import { Client } from 'discord.js';
|
||||
import { BanUpdate } from '../types';
|
||||
import { Database } from '../shared/Database';
|
||||
import { config } from '../config';
|
||||
|
||||
export async function banUpdateHandler(client: Client, update: string) {
|
||||
const data: BanUpdate = JSON.parse(update);
|
||||
|
||||
if (data.action == 'create') {
|
||||
kickDiscordAccounts(client, data);
|
||||
}
|
||||
// else if (data.action == 'delete') {
|
||||
// // unbanDiscordAccounts(data);
|
||||
// }
|
||||
}
|
||||
|
||||
async function kickDiscordAccounts(client: Client, data: BanUpdate) {
|
||||
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
for (const id of discordIds) {
|
||||
const member = await guild.members.fetch(id);
|
||||
|
||||
if (member) await member.kick(`Banned from e621 by: https://e621.net/users/${data.ban.banner_id}. Reason:\n${data.ban.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// async function banDiscordAccounts(data: BanUpdate) {
|
||||
// const guild = await discordClient.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
// const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
// for (const id of discordIds) {
|
||||
// await guild.bans.create(id, {
|
||||
// reason: data.ban.reason
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function unbanDiscordAccounts(data: BanUpdate) {
|
||||
// const guild = await discordClient.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
// const discordIds = await Database.getDiscordIds(data.ban.user_id);
|
||||
|
||||
// for (const id of discordIds) {
|
||||
// await guild.bans.remove(id);
|
||||
// }
|
||||
// }
|
||||
+21
-21
@@ -1,22 +1,22 @@
|
||||
import { Client } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { config } from '../config';
|
||||
|
||||
export async function checkExpiredBans(client: Client) {
|
||||
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) return;
|
||||
|
||||
const date = new Date();
|
||||
|
||||
for (const ban of await Database.getExpiredBans(date)) {
|
||||
try {
|
||||
await guild.bans.remove(ban.user_id);
|
||||
} catch (e) {
|
||||
console.error(`Error unbanning user: ${ban.user_id}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await Database.pruneExpiredBans(date);
|
||||
import { Client } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { config } from '../config';
|
||||
|
||||
export async function checkExpiredBans(client: Client) {
|
||||
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guild) return;
|
||||
|
||||
const date = new Date();
|
||||
|
||||
for (const ban of await Database.getExpiredBans(date)) {
|
||||
try {
|
||||
await guild.bans.remove(ban.user_id);
|
||||
} catch (e) {
|
||||
console.error(`Error unbanning user: ${ban.user_id}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await Database.pruneExpiredBans(date);
|
||||
}
|
||||
+27
-27
@@ -1,28 +1,28 @@
|
||||
import { GuildBasedChannel } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function channelIsInStaffCategory(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId || !channel.parentId) return false;
|
||||
|
||||
const staffCategories = await Database.getGuildArraySetting('staff_categories', channel.guildId);
|
||||
|
||||
const parentChannel = await channel.guild.channels.fetch(channel.parentId);
|
||||
|
||||
return parentChannel?.parentId ? staffCategories.includes(parentChannel.parentId) : staffCategories.includes(channel.parentId);
|
||||
}
|
||||
|
||||
export async function channelIsSafe(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId) return false;
|
||||
|
||||
const safeChannels = await Database.getGuildArraySetting('safe_channels', channel.guildId);
|
||||
|
||||
return safeChannels.includes(channel.id);
|
||||
}
|
||||
|
||||
export async function channelIgnoresLinks(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId) return false;
|
||||
|
||||
const linkSkipChannels = await Database.getGuildArraySetting('link_skip_channels', channel.guildId);
|
||||
|
||||
return linkSkipChannels.includes(channel.id) || channel.parentId ? linkSkipChannels.includes(channel.parentId!) : false;
|
||||
import { GuildBasedChannel } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function channelIsInStaffCategory(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId || !channel.parentId) return false;
|
||||
|
||||
const staffCategories = await Database.getGuildArraySetting('staff_categories', channel.guildId);
|
||||
|
||||
const parentChannel = await channel.guild.channels.fetch(channel.parentId);
|
||||
|
||||
return parentChannel?.parentId ? staffCategories.includes(parentChannel.parentId) : staffCategories.includes(channel.parentId);
|
||||
}
|
||||
|
||||
export async function channelIsSafe(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId) return false;
|
||||
|
||||
const safeChannels = await Database.getGuildArraySetting('safe_channels', channel.guildId);
|
||||
|
||||
return safeChannels.includes(channel.id);
|
||||
}
|
||||
|
||||
export async function channelIgnoresLinks(channel: GuildBasedChannel) {
|
||||
if (!channel.guildId) return false;
|
||||
|
||||
const linkSkipChannels = await Database.getGuildArraySetting('link_skip_channels', channel.guildId);
|
||||
|
||||
return linkSkipChannels.includes(channel.id) || channel.parentId ? linkSkipChannels.includes(channel.parentId!) : false;
|
||||
}
|
||||
+22
-22
@@ -1,22 +1,22 @@
|
||||
import fs from 'fs';
|
||||
import { Handler } from '../types';
|
||||
import path from 'path';
|
||||
import { Client } from 'discord.js';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
|
||||
export function loadHandlersFrom(dir: string, handlerArray: Handler[]) {
|
||||
if (!fs.existsSync(`${ROOT_DIR}/${dir}`)) return;
|
||||
|
||||
const files = fs.readdirSync(`${ROOT_DIR}/${dir}`).filter(file => file.endsWith('.js') || file.endsWith('.ts'));
|
||||
for (const file of files) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
handlerArray.push(require(`${ROOT_DIR}/${dir}/${file}`).default);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initIfNecessary(client: Client, handlers: Handler[]) {
|
||||
for (const handler of handlers) {
|
||||
if (handler.init) await handler.init(client);
|
||||
}
|
||||
}
|
||||
import fs from 'fs';
|
||||
import { Handler } from '../types';
|
||||
import path from 'path';
|
||||
import { Client } from 'discord.js';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
|
||||
export function loadHandlersFrom(dir: string, handlerArray: Handler[]) {
|
||||
if (!fs.existsSync(`${ROOT_DIR}/${dir}`)) return;
|
||||
|
||||
const files = fs.readdirSync(`${ROOT_DIR}/${dir}`).filter(file => file.endsWith('.js') || file.endsWith('.ts'));
|
||||
for (const file of files) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
handlerArray.push(require(`${ROOT_DIR}/${dir}/${file}`).default);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initIfNecessary(client: Client, handlers: Handler[]) {
|
||||
for (const handler of handlers) {
|
||||
if (handler.init) await handler.init(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from '../config';
|
||||
|
||||
export function logDebug(message: string) {
|
||||
if (config.DEBUG) console.log(`[DEBUG] ${message}`);
|
||||
import { config } from '../config';
|
||||
|
||||
export function logDebug(message: string) {
|
||||
if (config.DEBUG) console.log(`[DEBUG] ${message}`);
|
||||
}
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Client, Guild, User } from 'discord.js';
|
||||
import { Database, PrivateHelpTicketStatus } from '../shared/Database';
|
||||
|
||||
export async function resolveUser(client: Client, value: string, guild: Guild | null = null): Promise<User | null | undefined> {
|
||||
let user: User | null | undefined = null;
|
||||
|
||||
try {
|
||||
user = await client.users.fetch(value);
|
||||
} catch {
|
||||
user = client.users.cache.find(u => u.username == value);
|
||||
|
||||
if (!user && guild) {
|
||||
user = guild.members.cache.find(m => m.displayName == value)?.user;
|
||||
|
||||
if (!user) {
|
||||
try {
|
||||
const users = await guild.members.fetch({
|
||||
query: value
|
||||
});
|
||||
|
||||
if (users.size > 0) user = users.first()!.user;
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function canOpenPrivateHelpTicket(id: string): Promise<boolean> {
|
||||
const latestTicket = await Database.getLatestPrivateHelpTicketBy(id);
|
||||
|
||||
if (latestTicket && latestTicket.status == PrivateHelpTicketStatus.OPEN && Date.now() - new Date(latestTicket.timestamp).getTime() < 8.64e+7) return false;
|
||||
|
||||
return true;
|
||||
import { Client, Guild, User } from 'discord.js';
|
||||
import { Database, PrivateHelpTicketStatus } from '../shared/Database';
|
||||
|
||||
export async function resolveUser(client: Client, value: string, guild: Guild | null = null): Promise<User | null | undefined> {
|
||||
let user: User | null | undefined = null;
|
||||
|
||||
try {
|
||||
user = await client.users.fetch(value);
|
||||
} catch {
|
||||
user = client.users.cache.find(u => u.username == value);
|
||||
|
||||
if (!user && guild) {
|
||||
user = guild.members.cache.find(m => m.displayName == value)?.user;
|
||||
|
||||
if (!user) {
|
||||
try {
|
||||
const users = await guild.members.fetch({
|
||||
query: value
|
||||
});
|
||||
|
||||
if (users.size > 0) user = users.first()!.user;
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function canOpenPrivateHelpTicket(id: string): Promise<boolean> {
|
||||
const latestTicket = await Database.getLatestPrivateHelpTicketBy(id);
|
||||
|
||||
if (latestTicket && latestTicket.status == PrivateHelpTicketStatus.OPEN && Date.now() - new Date(latestTicket.timestamp).getTime() < 8.64e+7) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
+82
-82
@@ -1,83 +1,83 @@
|
||||
import { config } from '../config';
|
||||
import { E621Post, E621User, Record } from '../types';
|
||||
|
||||
const BLACKLISTED_TAGS: string[] = [];
|
||||
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
|
||||
|
||||
const SPOILERED_TAGS: string[] = ['gore', 'feces', 'watersports'];
|
||||
const SPOILERED_NONSAFE_TAGS: string[] = [];
|
||||
|
||||
const USER_AGENT = 'E621DiscordBot';
|
||||
|
||||
async function request(path: string, query?: { [name: string]: string }): Promise<any> {
|
||||
const url = new URL(config.E621_BASE_URL!);
|
||||
url.pathname = path + '.json';
|
||||
|
||||
if (query) {
|
||||
for (const [name, value] of Object.entries(query)) {
|
||||
url.searchParams.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function getE621User(idOrName: string | number): Promise<E621User | null> {
|
||||
return await request(`/users/${idOrName}`) as E621User;
|
||||
}
|
||||
|
||||
export async function getE621Post(id: string | number): Promise<E621Post | null> {
|
||||
return (await request(`/posts/${id}`))?.post as E621Post ?? null;
|
||||
}
|
||||
|
||||
export async function getE621PostByMd5(md5: string): Promise<E621Post | null> {
|
||||
return (await request('/posts', { md5 }))?.post as E621Post ?? null;
|
||||
}
|
||||
|
||||
export const enum PostAction {
|
||||
NoAction = 0,
|
||||
Spoiler = 1,
|
||||
Blacklist = 2
|
||||
}
|
||||
|
||||
export function spoilerOrBlacklist(post: E621Post): { action: PostAction, tag: string } {
|
||||
const tags = Object.values(post.tags).flat();
|
||||
|
||||
for (const tag of tags) {
|
||||
if (BLACKLISTED_TAGS.includes(tag)) return { action: PostAction.Blacklist, tag };
|
||||
if (post.rating != 's' && BLACKLISTED_NONSAFE_TAGS.includes(tag)) return { action: PostAction.Blacklist, tag };
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
if (SPOILERED_TAGS.includes(tag)) return { action: PostAction.Spoiler, tag };
|
||||
if (post.rating != 's' && SPOILERED_NONSAFE_TAGS.includes(tag)) return { action: PostAction.Spoiler, tag };
|
||||
}
|
||||
|
||||
return { action: PostAction.NoAction, tag: '' };
|
||||
}
|
||||
|
||||
export function getPostUrl(post: E621Post): string {
|
||||
if (post.rating == 's') return `${config.E926_BASE_URL}/posts/${post.id}`;
|
||||
return `${config.E621_BASE_URL}/posts/${post.id}`;
|
||||
}
|
||||
|
||||
export async function userIsBanned(idOrName: string | number): Promise<boolean> {
|
||||
const user = await getE621User(idOrName);
|
||||
return user?.is_banned ?? false;
|
||||
}
|
||||
|
||||
export async function getUserRecords(id: number): Promise<Record[]> {
|
||||
const records = await request('/user_feedbacks', { 'search[user_id]': id.toString() });
|
||||
|
||||
if (records.user_feedbacks) return [];
|
||||
|
||||
return records as Record[];
|
||||
import { config } from '../config';
|
||||
import { E621Post, E621User, Record } from '../types';
|
||||
|
||||
const BLACKLISTED_TAGS: string[] = [];
|
||||
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
|
||||
|
||||
const SPOILERED_TAGS: string[] = ['gore', 'feces', 'watersports'];
|
||||
const SPOILERED_NONSAFE_TAGS: string[] = [];
|
||||
|
||||
const USER_AGENT = 'E621DiscordBot';
|
||||
|
||||
async function request(path: string, query?: { [name: string]: string }): Promise<any> {
|
||||
const url = new URL(config.E621_BASE_URL!);
|
||||
url.pathname = path + '.json';
|
||||
|
||||
if (query) {
|
||||
for (const [name, value] of Object.entries(query)) {
|
||||
url.searchParams.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function getE621User(idOrName: string | number): Promise<E621User | null> {
|
||||
return await request(`/users/${idOrName}`) as E621User;
|
||||
}
|
||||
|
||||
export async function getE621Post(id: string | number): Promise<E621Post | null> {
|
||||
return (await request(`/posts/${id}`))?.post as E621Post ?? null;
|
||||
}
|
||||
|
||||
export async function getE621PostByMd5(md5: string): Promise<E621Post | null> {
|
||||
return (await request('/posts', { md5 }))?.post as E621Post ?? null;
|
||||
}
|
||||
|
||||
export const enum PostAction {
|
||||
NoAction = 0,
|
||||
Spoiler = 1,
|
||||
Blacklist = 2
|
||||
}
|
||||
|
||||
export function spoilerOrBlacklist(post: E621Post): { action: PostAction, tag: string } {
|
||||
const tags = Object.values(post.tags).flat();
|
||||
|
||||
for (const tag of tags) {
|
||||
if (BLACKLISTED_TAGS.includes(tag)) return { action: PostAction.Blacklist, tag };
|
||||
if (post.rating != 's' && BLACKLISTED_NONSAFE_TAGS.includes(tag)) return { action: PostAction.Blacklist, tag };
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
if (SPOILERED_TAGS.includes(tag)) return { action: PostAction.Spoiler, tag };
|
||||
if (post.rating != 's' && SPOILERED_NONSAFE_TAGS.includes(tag)) return { action: PostAction.Spoiler, tag };
|
||||
}
|
||||
|
||||
return { action: PostAction.NoAction, tag: '' };
|
||||
}
|
||||
|
||||
export function getPostUrl(post: E621Post): string {
|
||||
if (post.rating == 's') return `${config.E926_BASE_URL}/posts/${post.id}`;
|
||||
return `${config.E621_BASE_URL}/posts/${post.id}`;
|
||||
}
|
||||
|
||||
export async function userIsBanned(idOrName: string | number): Promise<boolean> {
|
||||
const user = await getE621User(idOrName);
|
||||
return user?.is_banned ?? false;
|
||||
}
|
||||
|
||||
export async function getUserRecords(id: number): Promise<Record[]> {
|
||||
const records = await request('/user_feedbacks', { 'search[user_id]': id.toString() });
|
||||
|
||||
if (records.user_feedbacks) return [];
|
||||
|
||||
return records as Record[];
|
||||
}
|
||||
+218
-218
@@ -1,219 +1,219 @@
|
||||
import { APIEmbedField, AttachmentBuilder, EmbedBuilder, Guild, GuildBasedChannel, GuildTextBasedChannel, MessageCreateOptions } from 'discord.js';
|
||||
import { Message } from '../events';
|
||||
import { Database } from '../shared/Database';
|
||||
import { LoggedMessage } from '../types';
|
||||
import { channelIsInStaffCategory } from './channel-utils';
|
||||
import { deserializeMessagePart, getModifiedAttachments, getModifiedStickers } from './message-utils';
|
||||
|
||||
type CustomEventLogData = {
|
||||
title: string
|
||||
description: string | null
|
||||
color: number | null
|
||||
timestamp: Date | number | null
|
||||
fields: APIEmbedField[] | null
|
||||
}
|
||||
|
||||
export async function logEdit(loggedMessage: LoggedMessage, newMessage: Message<true>) {
|
||||
const channel = await getEventLogChannel(newMessage.guild, newMessage.channel);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const includeContentInEmbed = loggedMessage.content.length <= 1024 && newMessage.content.length <= 1024;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, newMessage));
|
||||
fields.push(...getEditEmbeds(loggedMessage, newMessage, includeContentInEmbed));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Edited Message')
|
||||
.setColor(0xFFFF00)
|
||||
.setTimestamp(newMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
const messagePayload: MessageCreateOptions = { embeds: [embed] };
|
||||
|
||||
if (!includeContentInEmbed) {
|
||||
const before = new AttachmentBuilder(Buffer.from(loggedMessage.content), { name: 'before.txt' });
|
||||
const after = new AttachmentBuilder(Buffer.from(newMessage.content), { name: 'after.txt' });
|
||||
|
||||
messagePayload.files = [before, after];
|
||||
}
|
||||
|
||||
channel.send(messagePayload);
|
||||
}
|
||||
|
||||
export async function logDeletion(loggedMessage: LoggedMessage, deletedMessage: Message<true>) {
|
||||
const channel = await getEventLogChannel(deletedMessage.guild, deletedMessage.channel);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const includeContentInEmbed = loggedMessage.content.length <= 1024;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, deletedMessage));
|
||||
fields.push(...getDeletedEmbeds(loggedMessage, includeContentInEmbed));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Deleted Message')
|
||||
.setColor(0xFF0000)
|
||||
.setTimestamp(deletedMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
const messagePayload: MessageCreateOptions = { embeds: [embed] };
|
||||
|
||||
if (!includeContentInEmbed) {
|
||||
const before = new AttachmentBuilder(Buffer.from(loggedMessage.content), { name: 'content.txt' });
|
||||
|
||||
messagePayload.files = [before];
|
||||
}
|
||||
|
||||
channel.send(messagePayload);
|
||||
}
|
||||
|
||||
export async function logCustomEvent(guild: Guild, data: CustomEventLogData) {
|
||||
const channel = await getEventLogChannel(guild);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(data.title)
|
||||
.setColor(data.color)
|
||||
.setTimestamp(data.timestamp);
|
||||
|
||||
if (data.fields) embed.addFields(...data.fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function getEventLogChannel(guild: Guild, channel: GuildBasedChannel | null = null): Promise<GuildTextBasedChannel | null> {
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
if (channel && await channelIsInStaffCategory(channel)) {
|
||||
if (!settings.event_logs_channel_id) return null;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.event_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return null;
|
||||
|
||||
return channel;
|
||||
} else {
|
||||
if (!settings.discord_logs_channel_id) return null;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.discord_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return null;
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
function getMainEmbeds(loggedMessage: LoggedMessage, newMessage: Message<true>): APIEmbedField[] {
|
||||
const channelString = `${newMessage.channel.toString()}\n${newMessage.channel.name}`;
|
||||
const userString = `<@${loggedMessage.author_id}>\n${loggedMessage.author_name}`;
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Channel',
|
||||
value: channelString,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: userString,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
value: `[${newMessage.id}](${newMessage.url})`,
|
||||
inline: true
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getDeletedEmbeds(loggedMessage: LoggedMessage, includeContentInEmbed = true): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
|
||||
if (includeContentInEmbed && loggedMessage.content != '') {
|
||||
fields.push({
|
||||
name: 'Content',
|
||||
value: loggedMessage.content,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
for (const attachment of deserializeMessagePart(loggedMessage.attachments)) {
|
||||
fields.push({
|
||||
name: 'Attachment',
|
||||
value: attachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const sticker of deserializeMessagePart(loggedMessage.stickers)) {
|
||||
fields.push({
|
||||
name: 'Stickers',
|
||||
value: sticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getEditEmbeds(loggedMessage: LoggedMessage, newMessage: Message<true>, includeContentInEmbed = true): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
if (includeContentInEmbed && loggedMessage.content != newMessage.content) {
|
||||
fields.push(
|
||||
{
|
||||
name: 'Before',
|
||||
value: loggedMessage.content,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'After',
|
||||
value: newMessage.content,
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { addedAttachments, removedAttachments } = getModifiedAttachments(loggedMessage, newMessage);
|
||||
|
||||
for (const removedAttachment of removedAttachments) {
|
||||
fields.push({
|
||||
name: 'Removed Attachment',
|
||||
value: removedAttachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const addedAttachment of addedAttachments) {
|
||||
fields.push({
|
||||
name: 'Added Attachment',
|
||||
value: addedAttachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
const { addedStickers, removedStickers } = getModifiedStickers(loggedMessage, newMessage);
|
||||
|
||||
for (const removedSticker of addedStickers) {
|
||||
fields.push({
|
||||
name: 'Removed Sticker',
|
||||
value: removedSticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const addedSticker of removedStickers) {
|
||||
fields.push({
|
||||
name: 'Added Sticker',
|
||||
value: addedSticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
import { APIEmbedField, AttachmentBuilder, EmbedBuilder, Guild, GuildBasedChannel, GuildTextBasedChannel, MessageCreateOptions } from 'discord.js';
|
||||
import { Message } from '../events';
|
||||
import { Database } from '../shared/Database';
|
||||
import { LoggedMessage } from '../types';
|
||||
import { channelIsInStaffCategory } from './channel-utils';
|
||||
import { deserializeMessagePart, getModifiedAttachments, getModifiedStickers } from './message-utils';
|
||||
|
||||
type CustomEventLogData = {
|
||||
title: string
|
||||
description: string | null
|
||||
color: number | null
|
||||
timestamp: Date | number | null
|
||||
fields: APIEmbedField[] | null
|
||||
}
|
||||
|
||||
export async function logEdit(loggedMessage: LoggedMessage, newMessage: Message<true>) {
|
||||
const channel = await getEventLogChannel(newMessage.guild, newMessage.channel);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const includeContentInEmbed = loggedMessage.content.length <= 1024 && newMessage.content.length <= 1024;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, newMessage));
|
||||
fields.push(...getEditEmbeds(loggedMessage, newMessage, includeContentInEmbed));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Edited Message')
|
||||
.setColor(0xFFFF00)
|
||||
.setTimestamp(newMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
const messagePayload: MessageCreateOptions = { embeds: [embed] };
|
||||
|
||||
if (!includeContentInEmbed) {
|
||||
const before = new AttachmentBuilder(Buffer.from(loggedMessage.content), { name: 'before.txt' });
|
||||
const after = new AttachmentBuilder(Buffer.from(newMessage.content), { name: 'after.txt' });
|
||||
|
||||
messagePayload.files = [before, after];
|
||||
}
|
||||
|
||||
channel.send(messagePayload);
|
||||
}
|
||||
|
||||
export async function logDeletion(loggedMessage: LoggedMessage, deletedMessage: Message<true>) {
|
||||
const channel = await getEventLogChannel(deletedMessage.guild, deletedMessage.channel);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const includeContentInEmbed = loggedMessage.content.length <= 1024;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, deletedMessage));
|
||||
fields.push(...getDeletedEmbeds(loggedMessage, includeContentInEmbed));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Deleted Message')
|
||||
.setColor(0xFF0000)
|
||||
.setTimestamp(deletedMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
const messagePayload: MessageCreateOptions = { embeds: [embed] };
|
||||
|
||||
if (!includeContentInEmbed) {
|
||||
const before = new AttachmentBuilder(Buffer.from(loggedMessage.content), { name: 'content.txt' });
|
||||
|
||||
messagePayload.files = [before];
|
||||
}
|
||||
|
||||
channel.send(messagePayload);
|
||||
}
|
||||
|
||||
export async function logCustomEvent(guild: Guild, data: CustomEventLogData) {
|
||||
const channel = await getEventLogChannel(guild);
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(data.title)
|
||||
.setColor(data.color)
|
||||
.setTimestamp(data.timestamp);
|
||||
|
||||
if (data.fields) embed.addFields(...data.fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function getEventLogChannel(guild: Guild, channel: GuildBasedChannel | null = null): Promise<GuildTextBasedChannel | null> {
|
||||
const settings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
if (channel && await channelIsInStaffCategory(channel)) {
|
||||
if (!settings.event_logs_channel_id) return null;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.event_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return null;
|
||||
|
||||
return channel;
|
||||
} else {
|
||||
if (!settings.discord_logs_channel_id) return null;
|
||||
|
||||
const channel = await guild.channels.fetch(settings.discord_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return null;
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
function getMainEmbeds(loggedMessage: LoggedMessage, newMessage: Message<true>): APIEmbedField[] {
|
||||
const channelString = `${newMessage.channel.toString()}\n${newMessage.channel.name}`;
|
||||
const userString = `<@${loggedMessage.author_id}>\n${loggedMessage.author_name}`;
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Channel',
|
||||
value: channelString,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: userString,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
value: `[${newMessage.id}](${newMessage.url})`,
|
||||
inline: true
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getDeletedEmbeds(loggedMessage: LoggedMessage, includeContentInEmbed = true): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
|
||||
if (includeContentInEmbed && loggedMessage.content != '') {
|
||||
fields.push({
|
||||
name: 'Content',
|
||||
value: loggedMessage.content,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
for (const attachment of deserializeMessagePart(loggedMessage.attachments)) {
|
||||
fields.push({
|
||||
name: 'Attachment',
|
||||
value: attachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const sticker of deserializeMessagePart(loggedMessage.stickers)) {
|
||||
fields.push({
|
||||
name: 'Stickers',
|
||||
value: sticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getEditEmbeds(loggedMessage: LoggedMessage, newMessage: Message<true>, includeContentInEmbed = true): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
if (includeContentInEmbed && loggedMessage.content != newMessage.content) {
|
||||
fields.push(
|
||||
{
|
||||
name: 'Before',
|
||||
value: loggedMessage.content,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'After',
|
||||
value: newMessage.content,
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { addedAttachments, removedAttachments } = getModifiedAttachments(loggedMessage, newMessage);
|
||||
|
||||
for (const removedAttachment of removedAttachments) {
|
||||
fields.push({
|
||||
name: 'Removed Attachment',
|
||||
value: removedAttachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const addedAttachment of addedAttachments) {
|
||||
fields.push({
|
||||
name: 'Added Attachment',
|
||||
value: addedAttachment,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
const { addedStickers, removedStickers } = getModifiedStickers(loggedMessage, newMessage);
|
||||
|
||||
for (const removedSticker of addedStickers) {
|
||||
fields.push({
|
||||
name: 'Removed Sticker',
|
||||
value: removedSticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const addedSticker of removedStickers) {
|
||||
fields.push({
|
||||
name: 'Added Sticker',
|
||||
value: addedSticker,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
+70
-70
@@ -1,71 +1,71 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const DISCORD_PNG_ADDITIONAL_BYTE_LENGTH = 26;
|
||||
const END_PNG_BYTES = 12;
|
||||
|
||||
const DISCORD_JPG_START_OFFSET = 3;
|
||||
const DISCORD_JPG_REMOVE_BYTE_LENGTH = 23;
|
||||
const DISCORD_JPG_REINSERT = Buffer.from([0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]);
|
||||
const DISCORD_JPG_REINSERT_BYTE_LENGTH = DISCORD_JPG_REINSERT.byteLength;
|
||||
|
||||
export const ALLOWED_MIMETYPES = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'video/mp4', 'video/webm'];
|
||||
|
||||
export function calculateMD5(data: Buffer): string {
|
||||
return crypto.createHash('md5').update(data).digest('hex');
|
||||
}
|
||||
|
||||
// Downloads a file from discord's CDN and reverts the changes they do to the file.
|
||||
// Returns both the corrected version at index 0, and the original version from discord at index 1.
|
||||
export async function downloadFile(url: string): Promise<Buffer[] | null> {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
const mimeType = res.headers.get('Content-Type')!;
|
||||
|
||||
if (!ALLOWED_MIMETYPES.includes(mimeType)) return null;
|
||||
|
||||
const data = await res.arrayBuffer();
|
||||
|
||||
let finalData: Buffer;
|
||||
|
||||
if (mimeType == 'image/png') {
|
||||
const startOffset = data.byteLength - DISCORD_PNG_ADDITIONAL_BYTE_LENGTH - END_PNG_BYTES;
|
||||
const correctedData = Buffer.alloc(data.byteLength - DISCORD_PNG_ADDITIONAL_BYTE_LENGTH);
|
||||
const buff = Buffer.from(data);
|
||||
buff.copy(correctedData, 0, 0, startOffset);
|
||||
buff.copy(correctedData, startOffset, startOffset + DISCORD_PNG_ADDITIONAL_BYTE_LENGTH);
|
||||
finalData = correctedData;
|
||||
} else if (mimeType == 'image/jpg' || mimeType == 'image/jpeg') {
|
||||
const correctedData = Buffer.alloc(data.byteLength - DISCORD_JPG_REMOVE_BYTE_LENGTH + DISCORD_JPG_REINSERT_BYTE_LENGTH);
|
||||
const buff = Buffer.from(data);
|
||||
buff.copy(correctedData, 0, 0, DISCORD_JPG_START_OFFSET);
|
||||
DISCORD_JPG_REINSERT.copy(correctedData, DISCORD_JPG_START_OFFSET);
|
||||
buff.copy(correctedData, DISCORD_JPG_REMOVE_BYTE_LENGTH - DISCORD_JPG_START_OFFSET, DISCORD_JPG_START_OFFSET + DISCORD_JPG_REMOVE_BYTE_LENGTH);
|
||||
finalData = correctedData;
|
||||
} else {
|
||||
finalData = Buffer.from(data);
|
||||
}
|
||||
|
||||
return [finalData, Buffer.from(data)];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used with discord CDN URLs.
|
||||
// Discord does slight modifications to the data, which will change the MD5, this method reverts those changes.
|
||||
export async function calculateMD5FromURL(url: string): Promise<{ correctedFileMD5: string, originalFileMD5: string } | null> {
|
||||
try {
|
||||
const files = await downloadFile(url);
|
||||
if (!files) return null;
|
||||
|
||||
return {
|
||||
correctedFileMD5: calculateMD5(files[0]),
|
||||
originalFileMD5: calculateMD5(files[1])
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
import crypto from 'crypto';
|
||||
|
||||
const DISCORD_PNG_ADDITIONAL_BYTE_LENGTH = 26;
|
||||
const END_PNG_BYTES = 12;
|
||||
|
||||
const DISCORD_JPG_START_OFFSET = 3;
|
||||
const DISCORD_JPG_REMOVE_BYTE_LENGTH = 23;
|
||||
const DISCORD_JPG_REINSERT = Buffer.from([0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]);
|
||||
const DISCORD_JPG_REINSERT_BYTE_LENGTH = DISCORD_JPG_REINSERT.byteLength;
|
||||
|
||||
export const ALLOWED_MIMETYPES = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'video/mp4', 'video/webm'];
|
||||
|
||||
export function calculateMD5(data: Buffer): string {
|
||||
return crypto.createHash('md5').update(data).digest('hex');
|
||||
}
|
||||
|
||||
// Downloads a file from discord's CDN and reverts the changes they do to the file.
|
||||
// Returns both the corrected version at index 0, and the original version from discord at index 1.
|
||||
export async function downloadFile(url: string): Promise<Buffer[] | null> {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
const mimeType = res.headers.get('Content-Type')!;
|
||||
|
||||
if (!ALLOWED_MIMETYPES.includes(mimeType)) return null;
|
||||
|
||||
const data = await res.arrayBuffer();
|
||||
|
||||
let finalData: Buffer;
|
||||
|
||||
if (mimeType == 'image/png') {
|
||||
const startOffset = data.byteLength - DISCORD_PNG_ADDITIONAL_BYTE_LENGTH - END_PNG_BYTES;
|
||||
const correctedData = Buffer.alloc(data.byteLength - DISCORD_PNG_ADDITIONAL_BYTE_LENGTH);
|
||||
const buff = Buffer.from(data);
|
||||
buff.copy(correctedData, 0, 0, startOffset);
|
||||
buff.copy(correctedData, startOffset, startOffset + DISCORD_PNG_ADDITIONAL_BYTE_LENGTH);
|
||||
finalData = correctedData;
|
||||
} else if (mimeType == 'image/jpg' || mimeType == 'image/jpeg') {
|
||||
const correctedData = Buffer.alloc(data.byteLength - DISCORD_JPG_REMOVE_BYTE_LENGTH + DISCORD_JPG_REINSERT_BYTE_LENGTH);
|
||||
const buff = Buffer.from(data);
|
||||
buff.copy(correctedData, 0, 0, DISCORD_JPG_START_OFFSET);
|
||||
DISCORD_JPG_REINSERT.copy(correctedData, DISCORD_JPG_START_OFFSET);
|
||||
buff.copy(correctedData, DISCORD_JPG_REMOVE_BYTE_LENGTH - DISCORD_JPG_START_OFFSET, DISCORD_JPG_START_OFFSET + DISCORD_JPG_REMOVE_BYTE_LENGTH);
|
||||
finalData = correctedData;
|
||||
} else {
|
||||
finalData = Buffer.from(data);
|
||||
}
|
||||
|
||||
return [finalData, Buffer.from(data)];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used with discord CDN URLs.
|
||||
// Discord does slight modifications to the data, which will change the MD5, this method reverts those changes.
|
||||
export async function calculateMD5FromURL(url: string): Promise<{ correctedFileMD5: string, originalFileMD5: string } | null> {
|
||||
try {
|
||||
const files = await downloadFile(url);
|
||||
if (!files) return null;
|
||||
|
||||
return {
|
||||
correctedFileMD5: calculateMD5(files[0]),
|
||||
originalFileMD5: calculateMD5(files[1])
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
const mentionRegex = new RegExp('@([\\S]+),|@([\\S]+)', 'gi');
|
||||
const issueLinkRegex = new RegExp('\\s?\\(\\[(#\\d+)\\]\\(https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)\\)', 'gi');
|
||||
|
||||
export async function fixPings(body: string): Promise<string> {
|
||||
const mappings = await Database.getAllGithubUserMappings();
|
||||
|
||||
return body.replaceAll(mentionRegex, (match, m1, m2) => {
|
||||
const name = m1 ?? m2;
|
||||
const mapping = mappings.find(m => m.github_username == name);
|
||||
|
||||
return mapping ? `<@${mapping.discord_id}>${match.endsWith(',') ? ',' : ''}` : match;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeIssueLinks(body: string): string {
|
||||
return body.replaceAll(issueLinkRegex, '');
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
const mentionRegex = new RegExp('@([\\S]+),|@([\\S]+)', 'gi');
|
||||
const issueLinkRegex = new RegExp('\\s?\\(\\[(#\\d+)\\]\\(https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)\\)', 'gi');
|
||||
|
||||
export async function fixPings(body: string): Promise<string> {
|
||||
const mappings = await Database.getAllGithubUserMappings();
|
||||
|
||||
return body.replaceAll(mentionRegex, (match, m1, m2) => {
|
||||
const name = m1 ?? m2;
|
||||
const mapping = mappings.find(m => m.github_username == name);
|
||||
|
||||
return mapping ? `<@${mapping.discord_id}>${match.endsWith(',') ? ',' : ''}` : match;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeIssueLinks(body: string): string {
|
||||
return body.replaceAll(issueLinkRegex, '');
|
||||
}
|
||||
+29
-29
@@ -1,29 +1,29 @@
|
||||
export * from './alt-utils';
|
||||
export * from './array-utils';
|
||||
export * from './audit-log-utils';
|
||||
export * from './ban-events';
|
||||
export * from './ban-utils';
|
||||
export * from './channel-utils';
|
||||
export * from './commands';
|
||||
export * from './debug-utils';
|
||||
export * from './discord-user-utils';
|
||||
export * from './e621-utils';
|
||||
export * from './event-log-utils';
|
||||
export * from './file-utils';
|
||||
export * from './github-user-utils';
|
||||
export * from './interaction-utils';
|
||||
export * from './message-matcher-regex';
|
||||
export * from './message-utils';
|
||||
export * from './modal-utils';
|
||||
export * from './ms-to-human';
|
||||
export * from './name-sync';
|
||||
export * from './note-utils';
|
||||
export * from './oauth2';
|
||||
export * from './private-help-utils';
|
||||
export * from './record-utils';
|
||||
export * from './refresh-commands';
|
||||
export * from './string-utils';
|
||||
export * from './ticket-events';
|
||||
export * from './ticket-utils';
|
||||
export * from './wait';
|
||||
export * from './whois';
|
||||
export * from './alt-utils';
|
||||
export * from './array-utils';
|
||||
export * from './audit-log-utils';
|
||||
export * from './ban-events';
|
||||
export * from './ban-utils';
|
||||
export * from './channel-utils';
|
||||
export * from './commands';
|
||||
export * from './debug-utils';
|
||||
export * from './discord-user-utils';
|
||||
export * from './e621-utils';
|
||||
export * from './event-log-utils';
|
||||
export * from './file-utils';
|
||||
export * from './github-user-utils';
|
||||
export * from './interaction-utils';
|
||||
export * from './message-matcher-regex';
|
||||
export * from './message-utils';
|
||||
export * from './modal-utils';
|
||||
export * from './ms-to-human';
|
||||
export * from './name-sync';
|
||||
export * from './note-utils';
|
||||
export * from './oauth2';
|
||||
export * from './private-help-utils';
|
||||
export * from './record-utils';
|
||||
export * from './refresh-commands';
|
||||
export * from './string-utils';
|
||||
export * from './ticket-events';
|
||||
export * from './ticket-utils';
|
||||
export * from './wait';
|
||||
export * from './whois';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChatInputCommandInteraction, ContextMenuCommandInteraction, GuildBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { channelIsInStaffCategory } from './channel-utils';
|
||||
|
||||
export async function deferInteraction(interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction | ModalSubmitInteraction) {
|
||||
const isStaffChannel = await channelIsInStaffCategory(interaction.channel as GuildBasedChannel);
|
||||
|
||||
if (isStaffChannel) await interaction.deferReply();
|
||||
else await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
import { ChatInputCommandInteraction, ContextMenuCommandInteraction, GuildBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js';
|
||||
import { channelIsInStaffCategory } from './channel-utils';
|
||||
|
||||
export async function deferInteraction(interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction | ModalSubmitInteraction) {
|
||||
const isStaffChannel = await channelIsInStaffCategory(interaction.channel as GuildBasedChannel);
|
||||
|
||||
if (isStaffChannel) await interaction.deferReply();
|
||||
else await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
export const postIDRegex = new RegExp('post #([0-9]+)', 'gi');
|
||||
export const userIDRegex = new RegExp('user #([0-9]+)', 'gi');
|
||||
export const forumTopicIDRegex = new RegExp('topic #([0-9]+)', 'gi');
|
||||
export const commentIDRegex = new RegExp('comment #([0-9]+)', 'gi');
|
||||
export const blipIDRegex = new RegExp('blip #([0-9]+)', 'gi');
|
||||
export const poolIDRegex = new RegExp('pool #([0-9]+)', 'gi');
|
||||
export const setIDRegex = new RegExp('set #([0-9]+)', 'gi');
|
||||
export const takedownIDRegex = new RegExp('takedown #([0-9]+)', 'gi');
|
||||
export const recordIDRegex = new RegExp('record #([0-9]+)', 'gi');
|
||||
export const ticketIDRegex = new RegExp('ticket #([0-9]+)', 'gi');
|
||||
export const artistIDRegex = new RegExp('artist #([0-9]+)', 'gi');
|
||||
|
||||
const tagSearchRegex = '(?:[\\S]| )+?';
|
||||
export const wikiLinkRegex = new RegExp(`\\[\\[(${tagSearchRegex})]]`, 'gi');
|
||||
export const searchLinkRegex = new RegExp(`{{(${tagSearchRegex})}}`, 'gi');
|
||||
|
||||
export const prRegex = new RegExp('(?:pr|pull) #([0-9]+)', 'gi');
|
||||
export const postIDRegex = new RegExp('post #([0-9]+)', 'gi');
|
||||
export const userIDRegex = new RegExp('user #([0-9]+)', 'gi');
|
||||
export const forumTopicIDRegex = new RegExp('topic #([0-9]+)', 'gi');
|
||||
export const commentIDRegex = new RegExp('comment #([0-9]+)', 'gi');
|
||||
export const blipIDRegex = new RegExp('blip #([0-9]+)', 'gi');
|
||||
export const poolIDRegex = new RegExp('pool #([0-9]+)', 'gi');
|
||||
export const setIDRegex = new RegExp('set #([0-9]+)', 'gi');
|
||||
export const takedownIDRegex = new RegExp('takedown #([0-9]+)', 'gi');
|
||||
export const recordIDRegex = new RegExp('record #([0-9]+)', 'gi');
|
||||
export const ticketIDRegex = new RegExp('ticket #([0-9]+)', 'gi');
|
||||
export const artistIDRegex = new RegExp('artist #([0-9]+)', 'gi');
|
||||
|
||||
const tagSearchRegex = '(?:[\\S]| )+?';
|
||||
export const wikiLinkRegex = new RegExp(`\\[\\[(${tagSearchRegex})]]`, 'gi');
|
||||
export const searchLinkRegex = new RegExp(`{{(${tagSearchRegex})}}`, 'gi');
|
||||
|
||||
export const prRegex = new RegExp('(?:pr|pull) #([0-9]+)', 'gi');
|
||||
export const issueRegex = new RegExp('issue #([0-9]+)', 'gi');
|
||||
+59
-59
@@ -1,60 +1,60 @@
|
||||
import { Message } from '../events';
|
||||
import { LoggedMessage } from '../types';
|
||||
|
||||
export const ARRAY_SEPARATOR = '$';
|
||||
|
||||
const spoilerRegex = new RegExp('\\|\\|((?:[\\S]| )+?)\\|\\|', 'gi');
|
||||
|
||||
export function serializeMessage(message: Message): string[] {
|
||||
const attachments = message.attachments.map(a => `${a.name}:${a.id}`);
|
||||
const stickers = message.stickers.map(s => `${s.name}:${s.id}`);
|
||||
|
||||
return [message.id, message.author.id, message.author.username, message.channelId, attachments.join(ARRAY_SEPARATOR), stickers.join(ARRAY_SEPARATOR), message.content];
|
||||
}
|
||||
|
||||
export function deserializeMessagePart(part: string): string[] {
|
||||
return part.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
}
|
||||
|
||||
export function getModifiedAttachments(loggedMessage: LoggedMessage, newMessage: Message): { addedAttachments: string[], removedAttachments: string[] } {
|
||||
const loggedAttachments = loggedMessage.attachments.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
const addedAttachments = newMessage.attachments.filter(a => !loggedAttachments.includes(`${a.name}:${a.id}`)).map(a => `${a.name}:${a.id}`);
|
||||
const removedAttachments = loggedAttachments.filter(a => !newMessage.attachments.has(a.split(':').at(-1)!));
|
||||
|
||||
return { addedAttachments, removedAttachments };
|
||||
}
|
||||
|
||||
export function getModifiedStickers(loggedMessage: LoggedMessage, newMessage: Message): { addedStickers: string[], removedStickers: string[] } {
|
||||
const loggedStickers = loggedMessage.stickers.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
const addedStickers = newMessage.stickers.filter(s => !loggedStickers.includes(`${s.name}:${s.id}`)).map(s => `${s.name}:${s.id}`);
|
||||
const removedStickers = loggedStickers.filter(s => !newMessage.stickers.has(s.split(':').at(-1)!));
|
||||
|
||||
return { addedStickers, removedStickers };
|
||||
}
|
||||
|
||||
export function isEdited(loggedMessage: LoggedMessage, newMessage: Message) {
|
||||
if (newMessage.content != loggedMessage.content) return true;
|
||||
|
||||
const { addedAttachments, removedAttachments } = getModifiedAttachments(loggedMessage, newMessage);
|
||||
|
||||
if (addedAttachments.length > 0 || removedAttachments.length > 0) return true;
|
||||
|
||||
const { addedStickers, removedStickers } = getModifiedStickers(loggedMessage, newMessage);
|
||||
|
||||
return addedStickers.length > 0 || removedStickers.length > 0;
|
||||
}
|
||||
|
||||
export function isInSpoilerTags(content: string, index: number): boolean {
|
||||
if (!content.includes('||')) return false;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = spoilerRegex.exec(content)) != null) {
|
||||
if (index >= match.index && index <= spoilerRegex.lastIndex) {
|
||||
spoilerRegex.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
spoilerRegex.lastIndex = 0;
|
||||
return false;
|
||||
import { Message } from '../events';
|
||||
import { LoggedMessage } from '../types';
|
||||
|
||||
export const ARRAY_SEPARATOR = '$';
|
||||
|
||||
const spoilerRegex = new RegExp('\\|\\|((?:[\\S]| )+?)\\|\\|', 'gi');
|
||||
|
||||
export function serializeMessage(message: Message): string[] {
|
||||
const attachments = message.attachments.map(a => `${a.name}:${a.id}`);
|
||||
const stickers = message.stickers.map(s => `${s.name}:${s.id}`);
|
||||
|
||||
return [message.id, message.author.id, message.author.username, message.channelId, attachments.join(ARRAY_SEPARATOR), stickers.join(ARRAY_SEPARATOR), message.content];
|
||||
}
|
||||
|
||||
export function deserializeMessagePart(part: string): string[] {
|
||||
return part.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
}
|
||||
|
||||
export function getModifiedAttachments(loggedMessage: LoggedMessage, newMessage: Message): { addedAttachments: string[], removedAttachments: string[] } {
|
||||
const loggedAttachments = loggedMessage.attachments.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
const addedAttachments = newMessage.attachments.filter(a => !loggedAttachments.includes(`${a.name}:${a.id}`)).map(a => `${a.name}:${a.id}`);
|
||||
const removedAttachments = loggedAttachments.filter(a => !newMessage.attachments.has(a.split(':').at(-1)!));
|
||||
|
||||
return { addedAttachments, removedAttachments };
|
||||
}
|
||||
|
||||
export function getModifiedStickers(loggedMessage: LoggedMessage, newMessage: Message): { addedStickers: string[], removedStickers: string[] } {
|
||||
const loggedStickers = loggedMessage.stickers.split(ARRAY_SEPARATOR).filter(e => e);
|
||||
const addedStickers = newMessage.stickers.filter(s => !loggedStickers.includes(`${s.name}:${s.id}`)).map(s => `${s.name}:${s.id}`);
|
||||
const removedStickers = loggedStickers.filter(s => !newMessage.stickers.has(s.split(':').at(-1)!));
|
||||
|
||||
return { addedStickers, removedStickers };
|
||||
}
|
||||
|
||||
export function isEdited(loggedMessage: LoggedMessage, newMessage: Message) {
|
||||
if (newMessage.content != loggedMessage.content) return true;
|
||||
|
||||
const { addedAttachments, removedAttachments } = getModifiedAttachments(loggedMessage, newMessage);
|
||||
|
||||
if (addedAttachments.length > 0 || removedAttachments.length > 0) return true;
|
||||
|
||||
const { addedStickers, removedStickers } = getModifiedStickers(loggedMessage, newMessage);
|
||||
|
||||
return addedStickers.length > 0 || removedStickers.length > 0;
|
||||
}
|
||||
|
||||
export function isInSpoilerTags(content: string, index: number): boolean {
|
||||
if (!content.includes('||')) return false;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = spoilerRegex.exec(content)) != null) {
|
||||
if (index >= match.index && index <= spoilerRegex.lastIndex) {
|
||||
spoilerRegex.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
spoilerRegex.lastIndex = 0;
|
||||
return false;
|
||||
}
|
||||
+42
-42
@@ -1,43 +1,43 @@
|
||||
import { LabelBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
|
||||
|
||||
export function createTextInput(customId: string, labelTitle: string, description: string | null, required: boolean, style: TextInputStyle, maxLength: number | null, minLength: number | null): LabelBuilder {
|
||||
const input = new TextInputBuilder()
|
||||
.setCustomId(customId)
|
||||
.setStyle(style)
|
||||
.setRequired(required);
|
||||
|
||||
if (maxLength !== null) input.setMaxLength(maxLength);
|
||||
if (minLength !== null) input.setMinLength(minLength);
|
||||
|
||||
const label = new LabelBuilder()
|
||||
.setLabel(labelTitle);
|
||||
|
||||
if (description !== null) label.setDescription(description);
|
||||
|
||||
label.setTextInputComponent(input);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export function createYesNoMenu(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder {
|
||||
const yesNoMenu = new StringSelectMenuBuilder()
|
||||
.setCustomId(customId)
|
||||
.addOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel('Yes')
|
||||
.setDefault(defaultYes)
|
||||
.setValue('yes'),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel('No')
|
||||
.setDefault(!defaultYes)
|
||||
.setValue('no')
|
||||
);
|
||||
|
||||
const yesNoLabel = new LabelBuilder()
|
||||
.setLabel(labelTitle)
|
||||
.setStringSelectMenuComponent(yesNoMenu);
|
||||
|
||||
if (description !== null) yesNoLabel.setDescription(description);
|
||||
|
||||
return yesNoLabel;
|
||||
import { LabelBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
|
||||
|
||||
export function createTextInput(customId: string, labelTitle: string, description: string | null, required: boolean, style: TextInputStyle, maxLength: number | null, minLength: number | null): LabelBuilder {
|
||||
const input = new TextInputBuilder()
|
||||
.setCustomId(customId)
|
||||
.setStyle(style)
|
||||
.setRequired(required);
|
||||
|
||||
if (maxLength !== null) input.setMaxLength(maxLength);
|
||||
if (minLength !== null) input.setMinLength(minLength);
|
||||
|
||||
const label = new LabelBuilder()
|
||||
.setLabel(labelTitle);
|
||||
|
||||
if (description !== null) label.setDescription(description);
|
||||
|
||||
label.setTextInputComponent(input);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export function createYesNoMenu(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder {
|
||||
const yesNoMenu = new StringSelectMenuBuilder()
|
||||
.setCustomId(customId)
|
||||
.addOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel('Yes')
|
||||
.setDefault(defaultYes)
|
||||
.setValue('yes'),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel('No')
|
||||
.setDefault(!defaultYes)
|
||||
.setValue('no')
|
||||
);
|
||||
|
||||
const yesNoLabel = new LabelBuilder()
|
||||
.setLabel(labelTitle)
|
||||
.setStringSelectMenuComponent(yesNoMenu);
|
||||
|
||||
if (description !== null) yesNoLabel.setDescription(description);
|
||||
|
||||
return yesNoLabel;
|
||||
}
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
export function msToHuman(ms: number) {
|
||||
const time = {
|
||||
day: Math.floor(ms / 86400000),
|
||||
hour: Math.floor(ms / 3600000) % 24,
|
||||
minute: Math.floor(ms / 60000) % 60,
|
||||
second: Math.floor(ms / 1000) % 60,
|
||||
};
|
||||
return Object.entries(time)
|
||||
.filter(val => val[1] !== 0)
|
||||
.map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
|
||||
.join(', ');
|
||||
export function msToHuman(ms: number) {
|
||||
const time = {
|
||||
day: Math.floor(ms / 86400000),
|
||||
hour: Math.floor(ms / 3600000) % 24,
|
||||
minute: Math.floor(ms / 60000) % 60,
|
||||
second: Math.floor(ms / 1000) % 60,
|
||||
};
|
||||
return Object.entries(time)
|
||||
.filter(val => val[1] !== 0)
|
||||
.map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
|
||||
.join(', ');
|
||||
}
|
||||
+27
-27
@@ -1,28 +1,28 @@
|
||||
import { ChatInputCommandInteraction, GuildMember, ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { E621User } from '../types';
|
||||
import { getE621User } from './e621-utils';
|
||||
|
||||
export async function syncName(interaction: ChatInputCommandInteraction | UserContextMenuCommandInteraction | ModalSubmitInteraction, member: GuildMember, id: number | null) {
|
||||
if (member.roles.highest.comparePositionTo(member.guild.members.me!.roles.highest) > 0) {
|
||||
const res = interaction.user.id == member.id ? 'your' : 'their';
|
||||
return interaction.editReply(`I am unable to set ${res} nickname as ${res} role is higher than mine.`);
|
||||
}
|
||||
|
||||
const availableIds = await Database.getE621Ids(interaction.user.id);
|
||||
|
||||
let e621User: E621User | null;
|
||||
|
||||
if (!id || !availableIds.includes(id)) {
|
||||
e621User = await getE621User(availableIds[0]);
|
||||
} else {
|
||||
e621User = await getE621User(id);
|
||||
}
|
||||
|
||||
if (!e621User) {
|
||||
return interaction.editReply("Couldn't figure out what your name was. Please contact an administrator.");
|
||||
}
|
||||
|
||||
await member.setNickname(e621User.name);
|
||||
interaction.editReply(`Nickname set to: ${e621User.name}`);
|
||||
import { ChatInputCommandInteraction, GuildMember, ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { E621User } from '../types';
|
||||
import { getE621User } from './e621-utils';
|
||||
|
||||
export async function syncName(interaction: ChatInputCommandInteraction | UserContextMenuCommandInteraction | ModalSubmitInteraction, member: GuildMember, id: number | null) {
|
||||
if (member.roles.highest.comparePositionTo(member.guild.members.me!.roles.highest) > 0) {
|
||||
const res = interaction.user.id == member.id ? 'your' : 'their';
|
||||
return interaction.editReply(`I am unable to set ${res} nickname as ${res} role is higher than mine.`);
|
||||
}
|
||||
|
||||
const availableIds = await Database.getE621Ids(interaction.user.id);
|
||||
|
||||
let e621User: E621User | null;
|
||||
|
||||
if (!id || !availableIds.includes(id)) {
|
||||
e621User = await getE621User(availableIds[0]);
|
||||
} else {
|
||||
e621User = await getE621User(id);
|
||||
}
|
||||
|
||||
if (!e621User) {
|
||||
return interaction.editReply("Couldn't figure out what your name was. Please contact an administrator.");
|
||||
}
|
||||
|
||||
await member.setNickname(e621User.name);
|
||||
interaction.editReply(`Nickname set to: ${e621User.name}`);
|
||||
}
|
||||
+48
-48
@@ -1,49 +1,49 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, time } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { MessageContent, Note } from '../types';
|
||||
|
||||
const NOTES_PER_PAGE = 5;
|
||||
|
||||
function getNoteText(note: Note): string {
|
||||
const timestamp = time(new Date(note.timestamp));
|
||||
|
||||
return `### Note by <@${note.mod_id}> (${timestamp}):\n${note.reason}`;
|
||||
}
|
||||
|
||||
export async function getNoteMessage(userId: string, page: number): Promise<MessageContent | null> {
|
||||
const notes = (await Database.getNotes(userId)).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
page = page - 1;
|
||||
|
||||
const maxPage = Math.max(0, Math.ceil(notes.length / NOTES_PER_PAGE) - 1);
|
||||
|
||||
if (notes.length == 0) return null;
|
||||
|
||||
const noteTexts: string[] = [];
|
||||
|
||||
for (let i = page * NOTES_PER_PAGE; i < page * NOTES_PER_PAGE + NOTES_PER_PAGE; i++) {
|
||||
if (i >= notes.length) break;
|
||||
|
||||
noteTexts.push(getNoteText(notes[i]));
|
||||
}
|
||||
|
||||
const prevPage = new ButtonBuilder()
|
||||
.setLabel('Previous Page')
|
||||
.setCustomId(`note-previous_${userId}_${page + 1}`)
|
||||
.setDisabled(page == 0)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const nextPage = new ButtonBuilder()
|
||||
.setLabel('Next Page')
|
||||
.setCustomId(`note-next_${userId}_${page + 1}`)
|
||||
.setDisabled(page >= maxPage)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(prevPage, nextPage);
|
||||
|
||||
return {
|
||||
content: `<@${userId}>'s Notes\n` + noteTexts.join('\n\n') + `\n\n-# Page ${page + 1}/${maxPage + 1}`,
|
||||
components: [row]
|
||||
};
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, time } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { MessageContent, Note } from '../types';
|
||||
|
||||
const NOTES_PER_PAGE = 5;
|
||||
|
||||
function getNoteText(note: Note): string {
|
||||
const timestamp = time(new Date(note.timestamp));
|
||||
|
||||
return `### Note by <@${note.mod_id}> (${timestamp}):\n${note.reason}`;
|
||||
}
|
||||
|
||||
export async function getNoteMessage(userId: string, page: number): Promise<MessageContent | null> {
|
||||
const notes = (await Database.getNotes(userId)).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
page = page - 1;
|
||||
|
||||
const maxPage = Math.max(0, Math.ceil(notes.length / NOTES_PER_PAGE) - 1);
|
||||
|
||||
if (notes.length == 0) return null;
|
||||
|
||||
const noteTexts: string[] = [];
|
||||
|
||||
for (let i = page * NOTES_PER_PAGE; i < page * NOTES_PER_PAGE + NOTES_PER_PAGE; i++) {
|
||||
if (i >= notes.length) break;
|
||||
|
||||
noteTexts.push(getNoteText(notes[i]));
|
||||
}
|
||||
|
||||
const prevPage = new ButtonBuilder()
|
||||
.setLabel('Previous Page')
|
||||
.setCustomId(`note-previous_${userId}_${page + 1}`)
|
||||
.setDisabled(page == 0)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const nextPage = new ButtonBuilder()
|
||||
.setLabel('Next Page')
|
||||
.setCustomId(`note-next_${userId}_${page + 1}`)
|
||||
.setDisabled(page >= maxPage)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(prevPage, nextPage);
|
||||
|
||||
return {
|
||||
content: `<@${userId}>'s Notes\n` + noteTexts.join('\n\n') + `\n\n-# Page ${page + 1}/${maxPage + 1}`,
|
||||
components: [row]
|
||||
};
|
||||
}
|
||||
+136
-136
@@ -1,137 +1,137 @@
|
||||
type ClientOptions = {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
clientToken: string
|
||||
redirectUri: string
|
||||
credentials: string
|
||||
};
|
||||
|
||||
type GenerateUrlParameters = {
|
||||
state: string
|
||||
scope: string[]
|
||||
type: 'code' | 'token'
|
||||
};
|
||||
|
||||
type TokenResponse = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
refresh_token: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
type DiscordUser = {
|
||||
id: string
|
||||
username: string
|
||||
// bunch of other stuff we don't use
|
||||
}
|
||||
|
||||
type AddMemberOptions = {
|
||||
accessToken: string
|
||||
botToken?: string
|
||||
guildId: string
|
||||
userId: string
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
const OAUTH_BASE_URL = 'https://discord.com/oauth2';
|
||||
const OAUTH_API_BASE_URL = 'https://discord.com/api/oauth2';
|
||||
const API_BASE_URL = 'https://discord.com/api';
|
||||
|
||||
export class DiscordOAuth2 {
|
||||
constructor(private options: ClientOptions) { }
|
||||
|
||||
generateOauth2Url(options: GenerateUrlParameters) {
|
||||
const url = new URL(`${OAUTH_BASE_URL}/authorize`);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.options.clientId,
|
||||
response_type: options.type,
|
||||
redirect_uri: this.options.redirectUri,
|
||||
scope: options.scope.join('+'),
|
||||
state: options.state
|
||||
});
|
||||
|
||||
url.search = params.toString();
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async getAccessToken(code: string, scope: string[]): Promise<TokenResponse> {
|
||||
const res = await fetch(`${OAUTH_API_BASE_URL}/token`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: this.options.clientId,
|
||||
client_secret: this.options.clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: this.options.redirectUri,
|
||||
scope: scope.join(' ')
|
||||
}).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
return data as TokenResponse;
|
||||
}
|
||||
|
||||
async getUser(accessToken: string): Promise<DiscordUser> {
|
||||
const res = await fetch(`${API_BASE_URL}/users/@me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return await res.json() as DiscordUser;
|
||||
}
|
||||
|
||||
async addMember(options: AddMemberOptions) {
|
||||
const res = await fetch(`${API_BASE_URL}/guilds/${options.guildId}/members/${options.userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
nick: options.nickname,
|
||||
access_token: options.accessToken
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bot ${this.options.clientToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
console.error(`Non 200 code while joining user (${options.userId}) to discord (${res.status}):`);
|
||||
const text = await res.text();
|
||||
console.error(text);
|
||||
let data = { code: 0 };
|
||||
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch { }
|
||||
|
||||
throw data;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async revokeToken(token: string) {
|
||||
const res = await fetch(`${OAUTH_API_BASE_URL}/token/revoke`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
token
|
||||
}).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${this.options.credentials}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
type ClientOptions = {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
clientToken: string
|
||||
redirectUri: string
|
||||
credentials: string
|
||||
};
|
||||
|
||||
type GenerateUrlParameters = {
|
||||
state: string
|
||||
scope: string[]
|
||||
type: 'code' | 'token'
|
||||
};
|
||||
|
||||
type TokenResponse = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
refresh_token: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
type DiscordUser = {
|
||||
id: string
|
||||
username: string
|
||||
// bunch of other stuff we don't use
|
||||
}
|
||||
|
||||
type AddMemberOptions = {
|
||||
accessToken: string
|
||||
botToken?: string
|
||||
guildId: string
|
||||
userId: string
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
const OAUTH_BASE_URL = 'https://discord.com/oauth2';
|
||||
const OAUTH_API_BASE_URL = 'https://discord.com/api/oauth2';
|
||||
const API_BASE_URL = 'https://discord.com/api';
|
||||
|
||||
export class DiscordOAuth2 {
|
||||
constructor(private options: ClientOptions) { }
|
||||
|
||||
generateOauth2Url(options: GenerateUrlParameters) {
|
||||
const url = new URL(`${OAUTH_BASE_URL}/authorize`);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.options.clientId,
|
||||
response_type: options.type,
|
||||
redirect_uri: this.options.redirectUri,
|
||||
scope: options.scope.join('+'),
|
||||
state: options.state
|
||||
});
|
||||
|
||||
url.search = params.toString();
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async getAccessToken(code: string, scope: string[]): Promise<TokenResponse> {
|
||||
const res = await fetch(`${OAUTH_API_BASE_URL}/token`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: this.options.clientId,
|
||||
client_secret: this.options.clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: this.options.redirectUri,
|
||||
scope: scope.join(' ')
|
||||
}).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
return data as TokenResponse;
|
||||
}
|
||||
|
||||
async getUser(accessToken: string): Promise<DiscordUser> {
|
||||
const res = await fetch(`${API_BASE_URL}/users/@me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return await res.json() as DiscordUser;
|
||||
}
|
||||
|
||||
async addMember(options: AddMemberOptions) {
|
||||
const res = await fetch(`${API_BASE_URL}/guilds/${options.guildId}/members/${options.userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
nick: options.nickname,
|
||||
access_token: options.accessToken
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bot ${this.options.clientToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
console.error(`Non 200 code while joining user (${options.userId}) to discord (${res.status}):`);
|
||||
const text = await res.text();
|
||||
console.error(text);
|
||||
let data = { code: 0 };
|
||||
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch { }
|
||||
|
||||
throw data;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async revokeToken(token: string) {
|
||||
const res = await fetch(`${OAUTH_API_BASE_URL}/token/revoke`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
token
|
||||
}).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${this.options.credentials}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
}
|
||||
@@ -1,100 +1,100 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, ThreadChannel, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createTextInput, createYesNoMenu } from './modal-utils';
|
||||
|
||||
export async function closeOldTickets(client: Client) {
|
||||
for (const ticket of await Database.getAllOpenPrivateHelpTickets()) {
|
||||
try {
|
||||
const thread = await client.channels.fetch(ticket.thread_id) as ThreadChannel;
|
||||
const latestMessage = (await thread.messages.fetch({ limit: 1 })).at(0);
|
||||
if (latestMessage && latestMessage.createdTimestamp <= Date.now() - 432e6) {
|
||||
await Database.closePrivateHelpTicket(thread.id);
|
||||
|
||||
await thread.send('This ticket has been closed due to inactivity.');
|
||||
|
||||
thread.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error closing ticket due to inactivity:');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPrivateHelpTicketThread(client: Client, guild: Guild, creator: GuildMember | null, reason: string, customTitle: string = '', additionalMembersToAdd: string[] = []): Promise<PrivateThreadChannel | null> {
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_channel_id || !guildSettings.private_help_role_id) return null;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.private_help_channel_id) as TextChannel;
|
||||
|
||||
const thread = await channel.threads.create({
|
||||
name: customTitle ? customTitle : (creator ? `${creator.displayName}'s Ticket` : 'Mod Ticket'),
|
||||
autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
|
||||
invitable: false,
|
||||
type: ChannelType.PrivateThread
|
||||
}) as PrivateThreadChannel;
|
||||
|
||||
if (creator) await Database.createPrivateHelpTicket(creator.id, thread.id);
|
||||
|
||||
if (creator) {
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId('claim-ticket')
|
||||
.setLabel('Claim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, claimButton);
|
||||
|
||||
await thread.send({
|
||||
content: `${creator} feel free to direct your questions at any <@&${guildSettings.private_help_role_id}>. Only you and staff members can see this channel.\n\n**Reason for contact:**\n${reason}\n\n-# Tickets will automatically close after 5 days of inactivity.`,
|
||||
components: [row],
|
||||
allowedMentions: {
|
||||
users: [creator.id],
|
||||
roles: [guildSettings.private_help_role_id]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-mod-ticket')
|
||||
.setLabel('Close Mod Ticket')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton);
|
||||
|
||||
await thread.send({
|
||||
content: reason,
|
||||
components: [row],
|
||||
allowedMentions: {
|
||||
users: Array.from(new Set(additionalMembersToAdd))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of additionalMembersToAdd) {
|
||||
await thread.members.add(id);
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
export async function openModTicketModal(interaction: UserContextMenuCommandInteraction | ChatInputCommandInteraction, member: GuildMember) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`open-mod-ticket_${member.id}`)
|
||||
.setTitle('Opening A Mod Ticket');
|
||||
|
||||
const titleLabel = createTextInput('title', 'Title', `The name of the thread. Defaults to "Mod Ticket For ${member.displayName}" if left empty`, false, TextInputStyle.Short, 100, null);
|
||||
const initialMessageLabel = createTextInput('initial-message', 'Inital Message', 'The inital message sent in the thread', false, TextInputStyle.Paragraph, 1800, null);
|
||||
const autoJoinLabel = createYesNoMenu('auto-join-thread', 'Auto Join Thread', 'Whether or not to join you to the thread. Selecting no will not notify you of messages sent!', true);
|
||||
|
||||
modal.addLabelComponents(titleLabel, initialMessageLabel, autoJoinLabel);
|
||||
|
||||
interaction.showModal(modal);
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, ThreadChannel, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { createTextInput, createYesNoMenu } from './modal-utils';
|
||||
|
||||
export async function closeOldTickets(client: Client) {
|
||||
for (const ticket of await Database.getAllOpenPrivateHelpTickets()) {
|
||||
try {
|
||||
const thread = await client.channels.fetch(ticket.thread_id) as ThreadChannel;
|
||||
const latestMessage = (await thread.messages.fetch({ limit: 1 })).at(0);
|
||||
if (latestMessage && latestMessage.createdTimestamp <= Date.now() - 432e6) {
|
||||
await Database.closePrivateHelpTicket(thread.id);
|
||||
|
||||
await thread.send('This ticket has been closed due to inactivity.');
|
||||
|
||||
thread.edit({
|
||||
archived: true,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error closing ticket due to inactivity:');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPrivateHelpTicketThread(client: Client, guild: Guild, creator: GuildMember | null, reason: string, customTitle: string = '', additionalMembersToAdd: string[] = []): Promise<PrivateThreadChannel | null> {
|
||||
const guildSettings = await Database.getGuildSettings(guild.id);
|
||||
|
||||
if (!guildSettings || !guildSettings.private_help_channel_id || !guildSettings.private_help_role_id) return null;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.private_help_channel_id) as TextChannel;
|
||||
|
||||
const thread = await channel.threads.create({
|
||||
name: customTitle ? customTitle : (creator ? `${creator.displayName}'s Ticket` : 'Mod Ticket'),
|
||||
autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
|
||||
invitable: false,
|
||||
type: ChannelType.PrivateThread
|
||||
}) as PrivateThreadChannel;
|
||||
|
||||
if (creator) await Database.createPrivateHelpTicket(creator.id, thread.id);
|
||||
|
||||
if (creator) {
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-ticket')
|
||||
.setLabel('Click here if you no longer need help')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId('claim-ticket')
|
||||
.setLabel('Claim ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton, claimButton);
|
||||
|
||||
await thread.send({
|
||||
content: `${creator} feel free to direct your questions at any <@&${guildSettings.private_help_role_id}>. Only you and staff members can see this channel.\n\n**Reason for contact:**\n${reason}\n\n-# Tickets will automatically close after 5 days of inactivity.`,
|
||||
components: [row],
|
||||
allowedMentions: {
|
||||
users: [creator.id],
|
||||
roles: [guildSettings.private_help_role_id]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const closeButton = new ButtonBuilder()
|
||||
.setCustomId('close-mod-ticket')
|
||||
.setLabel('Close Mod Ticket')
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(closeButton);
|
||||
|
||||
await thread.send({
|
||||
content: reason,
|
||||
components: [row],
|
||||
allowedMentions: {
|
||||
users: Array.from(new Set(additionalMembersToAdd))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of additionalMembersToAdd) {
|
||||
await thread.members.add(id);
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
export async function openModTicketModal(interaction: UserContextMenuCommandInteraction | ChatInputCommandInteraction, member: GuildMember) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`open-mod-ticket_${member.id}`)
|
||||
.setTitle('Opening A Mod Ticket');
|
||||
|
||||
const titleLabel = createTextInput('title', 'Title', `The name of the thread. Defaults to "Mod Ticket For ${member.displayName}" if left empty`, false, TextInputStyle.Short, 100, null);
|
||||
const initialMessageLabel = createTextInput('initial-message', 'Inital Message', 'The inital message sent in the thread', false, TextInputStyle.Paragraph, 1800, null);
|
||||
const autoJoinLabel = createYesNoMenu('auto-join-thread', 'Auto Join Thread', 'Whether or not to join you to the thread. Selecting no will not notify you of messages sent!', true);
|
||||
|
||||
modal.addLabelComponents(titleLabel, initialMessageLabel, autoJoinLabel);
|
||||
|
||||
interaction.showModal(modal);
|
||||
}
|
||||
+102
-102
@@ -1,103 +1,103 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, Guild } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { E621User, MessageContent, Record, RecordCategory } from '../types';
|
||||
import { comprehensiveAltLookupFromDiscord, e621IdsFromAltData } from './alt-utils';
|
||||
import { getE621User, getUserRecords } from './e621-utils';
|
||||
|
||||
type RecordWithUserData = Record & { user: E621User, creator: E621User, updater: E621User }
|
||||
type AllRecords = RecordWithUserData[];
|
||||
|
||||
const RECORDS_PER_PAGE = 5;
|
||||
|
||||
export async function getAllRecordsFromDiscordId(id: string, guild: Guild): Promise<AllRecords> {
|
||||
const altData = await comprehensiveAltLookupFromDiscord(id, guild);
|
||||
const userCache: Map<number, E621User> = new Map();
|
||||
|
||||
const allRecords: AllRecords = [];
|
||||
|
||||
const e621UserIds = e621IdsFromAltData(altData);
|
||||
|
||||
for (const id of e621UserIds) {
|
||||
const user = userCache.get(id) ?? await getE621User(id);
|
||||
if (!user) continue;
|
||||
userCache.set(id, user);
|
||||
|
||||
const records = await getUserRecords(id);
|
||||
for (const record of records) {
|
||||
const creator = userCache.get(record.creator_id) ?? await getE621User(record.creator_id);
|
||||
if (!creator) continue;
|
||||
userCache.set(record.creator_id, creator);
|
||||
|
||||
const updater = userCache.get(record.updater_id) ?? await getE621User(record.updater_id);
|
||||
if (!updater) continue;
|
||||
userCache.set(record.updater_id, updater);
|
||||
|
||||
allRecords.push({
|
||||
user,
|
||||
creator,
|
||||
updater,
|
||||
...record
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
}
|
||||
|
||||
export async function getRecordMessageFromDiscordId(id: string, page: number, guild: Guild): Promise<MessageContent | null> {
|
||||
const records = await getAllRecordsFromDiscordId(id, guild);
|
||||
|
||||
if (records.length == 0) return null;
|
||||
|
||||
page = page - 1;
|
||||
|
||||
const maxPage = Math.floor(records.length / RECORDS_PER_PAGE);
|
||||
|
||||
const embeds: EmbedBuilder[] = [];
|
||||
|
||||
for (let i = page * RECORDS_PER_PAGE; i < page * RECORDS_PER_PAGE + RECORDS_PER_PAGE; i++) {
|
||||
if (i >= records.length) break;
|
||||
|
||||
embeds.push(getRecordEmbed(records[i]));
|
||||
}
|
||||
|
||||
const prevPage = new ButtonBuilder()
|
||||
.setLabel('Previous Page')
|
||||
.setCustomId(`records-previous_${id}_${page + 1}`)
|
||||
.setDisabled(page == 0)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const nextPage = new ButtonBuilder()
|
||||
.setLabel('Next Page')
|
||||
.setCustomId(`records-next_${id}_${page + 1}`)
|
||||
.setDisabled(page >= maxPage)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(prevPage, nextPage);
|
||||
|
||||
return { content: `Records found for <@${id}>`, embeds, components: [row] };
|
||||
}
|
||||
|
||||
function getRecordColor(category: RecordCategory) {
|
||||
switch (category) {
|
||||
case 'positive': return 0x00ff00;
|
||||
case 'negative': return 0xff0000;
|
||||
case 'neutral': return 0xaaaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
function getRecordEmbed(record: RecordWithUserData): EmbedBuilder {
|
||||
const isUpdated = record.updated_at != record.created_at;
|
||||
const creator = isUpdated ? record.updater : record.creator;
|
||||
return new EmbedBuilder()
|
||||
.setColor(getRecordColor(record.category))
|
||||
.setTitle(`Record from ${record.creator.name} for ${record.user.name}`)
|
||||
.setDescription(record.body.trim())
|
||||
.setURL(`${config.E621_BASE_URL}/user_feedbacks/${record.id}`)
|
||||
.setAuthor({
|
||||
name: `${isUpdated ? 'Last updated by' : 'Created by'}: ${creator.name}`,
|
||||
url: `${config.E621_BASE_URL}/users/${creator.id}`
|
||||
})
|
||||
.setTimestamp(new Date(record.updated_at));
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, Guild } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { E621User, MessageContent, Record, RecordCategory } from '../types';
|
||||
import { comprehensiveAltLookupFromDiscord, e621IdsFromAltData } from './alt-utils';
|
||||
import { getE621User, getUserRecords } from './e621-utils';
|
||||
|
||||
type RecordWithUserData = Record & { user: E621User, creator: E621User, updater: E621User }
|
||||
type AllRecords = RecordWithUserData[];
|
||||
|
||||
const RECORDS_PER_PAGE = 5;
|
||||
|
||||
export async function getAllRecordsFromDiscordId(id: string, guild: Guild): Promise<AllRecords> {
|
||||
const altData = await comprehensiveAltLookupFromDiscord(id, guild);
|
||||
const userCache: Map<number, E621User> = new Map();
|
||||
|
||||
const allRecords: AllRecords = [];
|
||||
|
||||
const e621UserIds = e621IdsFromAltData(altData);
|
||||
|
||||
for (const id of e621UserIds) {
|
||||
const user = userCache.get(id) ?? await getE621User(id);
|
||||
if (!user) continue;
|
||||
userCache.set(id, user);
|
||||
|
||||
const records = await getUserRecords(id);
|
||||
for (const record of records) {
|
||||
const creator = userCache.get(record.creator_id) ?? await getE621User(record.creator_id);
|
||||
if (!creator) continue;
|
||||
userCache.set(record.creator_id, creator);
|
||||
|
||||
const updater = userCache.get(record.updater_id) ?? await getE621User(record.updater_id);
|
||||
if (!updater) continue;
|
||||
userCache.set(record.updater_id, updater);
|
||||
|
||||
allRecords.push({
|
||||
user,
|
||||
creator,
|
||||
updater,
|
||||
...record
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
}
|
||||
|
||||
export async function getRecordMessageFromDiscordId(id: string, page: number, guild: Guild): Promise<MessageContent | null> {
|
||||
const records = await getAllRecordsFromDiscordId(id, guild);
|
||||
|
||||
if (records.length == 0) return null;
|
||||
|
||||
page = page - 1;
|
||||
|
||||
const maxPage = Math.floor(records.length / RECORDS_PER_PAGE);
|
||||
|
||||
const embeds: EmbedBuilder[] = [];
|
||||
|
||||
for (let i = page * RECORDS_PER_PAGE; i < page * RECORDS_PER_PAGE + RECORDS_PER_PAGE; i++) {
|
||||
if (i >= records.length) break;
|
||||
|
||||
embeds.push(getRecordEmbed(records[i]));
|
||||
}
|
||||
|
||||
const prevPage = new ButtonBuilder()
|
||||
.setLabel('Previous Page')
|
||||
.setCustomId(`records-previous_${id}_${page + 1}`)
|
||||
.setDisabled(page == 0)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const nextPage = new ButtonBuilder()
|
||||
.setLabel('Next Page')
|
||||
.setCustomId(`records-next_${id}_${page + 1}`)
|
||||
.setDisabled(page >= maxPage)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(prevPage, nextPage);
|
||||
|
||||
return { content: `Records found for <@${id}>`, embeds, components: [row] };
|
||||
}
|
||||
|
||||
function getRecordColor(category: RecordCategory) {
|
||||
switch (category) {
|
||||
case 'positive': return 0x00ff00;
|
||||
case 'negative': return 0xff0000;
|
||||
case 'neutral': return 0xaaaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
function getRecordEmbed(record: RecordWithUserData): EmbedBuilder {
|
||||
const isUpdated = record.updated_at != record.created_at;
|
||||
const creator = isUpdated ? record.updater : record.creator;
|
||||
return new EmbedBuilder()
|
||||
.setColor(getRecordColor(record.category))
|
||||
.setTitle(`Record from ${record.creator.name} for ${record.user.name}`)
|
||||
.setDescription(record.body.trim())
|
||||
.setURL(`${config.E621_BASE_URL}/user_feedbacks/${record.id}`)
|
||||
.setAuthor({
|
||||
name: `${isUpdated ? 'Last updated by' : 'Created by'}: ${creator.name}`,
|
||||
url: `${config.E621_BASE_URL}/users/${creator.id}`
|
||||
})
|
||||
.setTimestamp(new Date(record.updated_at));
|
||||
}
|
||||
@@ -1,71 +1,71 @@
|
||||
import { Client, REST, Routes } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { RESTPostAPIApplicationCommandsJSONBody } from 'discord.js';
|
||||
import fs from 'fs';
|
||||
import { Command } from '../types';
|
||||
import path from 'path';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(config.DISCORD_TOKEN!);
|
||||
|
||||
export async function refreshCommands(client: Client) {
|
||||
try {
|
||||
const commands: RESTPostAPIApplicationCommandsJSONBody[] = [];
|
||||
const guildCommands: { [id: string]: RESTPostAPIApplicationCommandsJSONBody[] } = {};
|
||||
const commandFiles = {
|
||||
commands: fs.readdirSync(`${ROOT_DIR}/commands`).filter(file => file.endsWith('.js') || file.endsWith('.ts')),
|
||||
'context-menus': fs.readdirSync(`${ROOT_DIR}/context-menus`).filter(file => file.endsWith('.js') || file.endsWith('.ts')),
|
||||
};
|
||||
|
||||
for (const [folderName, files] of Object.entries(commandFiles)) {
|
||||
for (const file of files) {
|
||||
const p = `${ROOT_DIR}/${folderName}/${file}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const command: Command = require(p).default;
|
||||
|
||||
if (!command) {
|
||||
console.warn(`File at ${p} has no export. Skipping registering.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let data: RESTPostAPIApplicationCommandsJSONBody;
|
||||
if (typeof (command.data) == 'function') {
|
||||
data = (await command.data(client)).toJSON();
|
||||
} else {
|
||||
data = command.data.toJSON();
|
||||
}
|
||||
|
||||
if (!command.guilds) {
|
||||
commands.push(data);
|
||||
} else {
|
||||
for (const id of command.guilds) {
|
||||
if (!guildCommands[id]) guildCommands[id] = [];
|
||||
guildCommands[id].push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
|
||||
console.log('Global commands: ' + commands.length);
|
||||
await rest.put(
|
||||
Routes.applicationCommands(config.DISCORD_CLIENT_ID!),
|
||||
{ body: commands }
|
||||
);
|
||||
|
||||
for (const guild in guildCommands) {
|
||||
console.log('Guild commands: ' + guildCommands[guild].length + ' (' + guild + ')');
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(config.DISCORD_CLIENT_ID!, guild),
|
||||
{ body: guildCommands[guild] }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
console.error(JSON.stringify(error.requestBody, null, 4));
|
||||
}
|
||||
import { Client, REST, Routes } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { RESTPostAPIApplicationCommandsJSONBody } from 'discord.js';
|
||||
import fs from 'fs';
|
||||
import { Command } from '../types';
|
||||
import path from 'path';
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(config.DISCORD_TOKEN!);
|
||||
|
||||
export async function refreshCommands(client: Client) {
|
||||
try {
|
||||
const commands: RESTPostAPIApplicationCommandsJSONBody[] = [];
|
||||
const guildCommands: { [id: string]: RESTPostAPIApplicationCommandsJSONBody[] } = {};
|
||||
const commandFiles = {
|
||||
commands: fs.readdirSync(`${ROOT_DIR}/commands`).filter(file => file.endsWith('.js') || file.endsWith('.ts')),
|
||||
'context-menus': fs.readdirSync(`${ROOT_DIR}/context-menus`).filter(file => file.endsWith('.js') || file.endsWith('.ts')),
|
||||
};
|
||||
|
||||
for (const [folderName, files] of Object.entries(commandFiles)) {
|
||||
for (const file of files) {
|
||||
const p = `${ROOT_DIR}/${folderName}/${file}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const command: Command = require(p).default;
|
||||
|
||||
if (!command) {
|
||||
console.warn(`File at ${p} has no export. Skipping registering.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let data: RESTPostAPIApplicationCommandsJSONBody;
|
||||
if (typeof (command.data) == 'function') {
|
||||
data = (await command.data(client)).toJSON();
|
||||
} else {
|
||||
data = command.data.toJSON();
|
||||
}
|
||||
|
||||
if (!command.guilds) {
|
||||
commands.push(data);
|
||||
} else {
|
||||
for (const id of command.guilds) {
|
||||
if (!guildCommands[id]) guildCommands[id] = [];
|
||||
guildCommands[id].push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
|
||||
console.log('Global commands: ' + commands.length);
|
||||
await rest.put(
|
||||
Routes.applicationCommands(config.DISCORD_CLIENT_ID!),
|
||||
{ body: commands }
|
||||
);
|
||||
|
||||
for (const guild in guildCommands) {
|
||||
console.log('Guild commands: ' + guildCommands[guild].length + ' (' + guild + ')');
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(config.DISCORD_CLIENT_ID!, guild),
|
||||
{ body: guildCommands[guild] }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
console.error(JSON.stringify(error.requestBody, null, 4));
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export function humanizeCapitalization(str: string): string {
|
||||
return str.toLowerCase()
|
||||
.split(' ')
|
||||
.map(s => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(' ');
|
||||
export function humanizeCapitalization(str: string): string {
|
||||
return str.toLowerCase()
|
||||
.split(' ')
|
||||
.map(s => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
+380
-380
@@ -1,381 +1,381 @@
|
||||
import { APIEmbedField, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedAuthorOptions, EmbedBuilder, SendableChannels } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { Ticket, TicketPhrase, TicketUpdate } from '../types';
|
||||
import { PostAction, getE621Post, getE621User, spoilerOrBlacklist } from './e621-utils';
|
||||
import { blipIDRegex, commentIDRegex, forumTopicIDRegex, poolIDRegex, postIDRegex, recordIDRegex, searchLinkRegex, setIDRegex, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from './message-matcher-regex';
|
||||
import { humanizeCapitalization } from './string-utils';
|
||||
import { shouldAlert } from './ticket-utils';
|
||||
|
||||
// TODO: Condense this and the message event handler regex array.
|
||||
const linkReplacers = [
|
||||
{
|
||||
regex: blipIDRegex,
|
||||
replacement: '/blips/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: commentIDRegex,
|
||||
replacement: '/comments/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: forumTopicIDRegex,
|
||||
replacement: '/forum_topics/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: poolIDRegex,
|
||||
replacement: '/pools/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: postIDRegex,
|
||||
tester: async (postId: string, before: string, after: string) => {
|
||||
const post = await getE621Post(postId);
|
||||
if (!post) return { allowed: true, before, after };
|
||||
const allowed = spoilerOrBlacklist(post).action != PostAction.Blacklist;
|
||||
|
||||
return { allowed, before, after };
|
||||
},
|
||||
replacement: '/posts/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: recordIDRegex,
|
||||
replacement: '/user_feedbacks/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: searchLinkRegex,
|
||||
replacement: '/posts?tags={match}',
|
||||
encodeURI: true
|
||||
},
|
||||
{
|
||||
regex: setIDRegex,
|
||||
replacement: '/post_sets/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: takedownIDRegex,
|
||||
replacement: '/takedowns/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: ticketIDRegex,
|
||||
replacement: '/tickets/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: userIDRegex,
|
||||
replacement: '/users/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: wikiLinkRegex,
|
||||
replacement: '/wiki_pages/{match}',
|
||||
encodeURI: true
|
||||
}
|
||||
];
|
||||
|
||||
const urlRegex = new RegExp('"((?:[\\S]| )+?)":\\[?((?:https?:\\/\\/[\\w\\d.\\/?=#&%]+)|\\/[\\w\\d.\\/?=#\\[\\]]+)\\]?', 'gi');
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
export async function ticketUpdateHandler(client: Client, update: string) {
|
||||
const data: TicketUpdate = JSON.parse(update);
|
||||
|
||||
if (data.action == 'create') {
|
||||
postTicket(client, data);
|
||||
} else {
|
||||
updateTicket(client, data);
|
||||
}
|
||||
}
|
||||
|
||||
async function postTicket(client: Client, data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const ticket = data.ticket;
|
||||
|
||||
const embed = await createEmbedFromTicket(ticket);
|
||||
|
||||
const row = await getButtons(ticket);
|
||||
|
||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||
|
||||
await Database.putTicket(ticket.id, message.id);
|
||||
|
||||
sendTicketAlerts(ticket, channel);
|
||||
}
|
||||
|
||||
async function updateTicket(client: Client, data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const messageId = await Database.getTicketMessageId(data.ticket.id);
|
||||
|
||||
if (!messageId) return postTicket(client, data);
|
||||
|
||||
const message = await channel.messages.fetch(messageId);
|
||||
const embed = await createEmbedFromTicket(data.ticket);
|
||||
|
||||
if (!message || message.author.id != config.DISCORD_CLIENT_ID) {
|
||||
const newMessage = await channel.send({ embeds: [embed] });
|
||||
await Database.removeTicket(data.ticket.id);
|
||||
await Database.putTicket(data.ticket.id, newMessage.id);
|
||||
} else {
|
||||
await message.edit({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
function getTitle(ticket: Ticket): string {
|
||||
if (!ticket.target) return `${humanizeCapitalization(ticket.category)} report by ${ticket.user}`;
|
||||
|
||||
switch (ticket.category) {
|
||||
case 'blip':
|
||||
return `Blip by ${ticket.target}`;
|
||||
case 'comment':
|
||||
return `Comment by ${ticket.target}`;
|
||||
case 'dmail':
|
||||
return `DMail sent by ${ticket.target}`;
|
||||
case 'forum':
|
||||
return `Forum post by ${ticket.target}`;
|
||||
case 'pool':
|
||||
return `Pool ${ticket.target}`;
|
||||
case 'post':
|
||||
return `Post uploaded by ${ticket.target}`;
|
||||
case 'set':
|
||||
return `Wow, a rare set report! ${ticket.target}`;
|
||||
case 'user':
|
||||
return `User ${ticket.target}`;
|
||||
case 'wiki':
|
||||
return `Wiki page ${ticket.target}`;
|
||||
default:
|
||||
return 'Uknown ticket category';
|
||||
}
|
||||
}
|
||||
|
||||
function getURL(ticket: Ticket): string {
|
||||
return `${config.E621_BASE_URL}/tickets/${ticket.id}`;
|
||||
}
|
||||
|
||||
async function getLinks(input: string, limit: number = Number.MAX_SAFE_INTEGER): Promise<string> {
|
||||
const length = input.length;
|
||||
|
||||
const replacedIndexes: { start: number, end: number }[] = [];
|
||||
const checks: Promise<{ allowed: boolean, before: string, after: string }>[] = [];
|
||||
|
||||
for (const replacer of linkReplacers) {
|
||||
input = input.replaceAll(replacer.regex, (match, group1) => {
|
||||
const replaced = `[${match}](${config.E621_BASE_URL}${(replacer.replacement).replace('{match}', replacer.encodeURI ? encodeURIComponent(group1) : group1)})`;
|
||||
if (replacer.tester) checks.push(replacer.tester(group1, match, replaced));
|
||||
const start = input.indexOf(match);
|
||||
replacedIndexes.push({ start, end: start + replaced.length });
|
||||
|
||||
return replaced;
|
||||
});
|
||||
}
|
||||
|
||||
input = input.replaceAll(urlRegex, (match, group1, group2) => {
|
||||
const replaced = group2.startsWith('/') ? `[${group1}](${config.E621_BASE_URL}${group2})` : `[${group1}](${group2})`;
|
||||
const start = input.indexOf(match);
|
||||
replacedIndexes.push({ start, end: start + replaced.length });
|
||||
return replaced;
|
||||
});
|
||||
|
||||
const values = await Promise.all(checks);
|
||||
|
||||
for (const check of values) {
|
||||
if (!check.allowed) {
|
||||
input = input.replace(check.after, check.before);
|
||||
}
|
||||
}
|
||||
|
||||
if (length > limit) {
|
||||
for (const replacedIndex of replacedIndexes) {
|
||||
if (replacedIndex.start < limit && replacedIndex.end >= limit) {
|
||||
return input.substring(0, replacedIndex.end) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
return input.substring(0, limit) + '...';
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
async function getDescription(ticket: Ticket): Promise<string> {
|
||||
return ticket.reason.length <= MAX_DESCRIPTION_LENGTH ? await getLinks(ticket.reason) : await getLinks(ticket.reason, MAX_DESCRIPTION_LENGTH);
|
||||
}
|
||||
|
||||
function getAuthor(ticket: Ticket): EmbedAuthorOptions {
|
||||
return {
|
||||
url: `${config.E621_BASE_URL}/users/${ticket.user_id}`,
|
||||
name: ticket.user
|
||||
};
|
||||
}
|
||||
|
||||
function getColor(ticket: Ticket): number {
|
||||
if (!ticket.claimant) {
|
||||
return 0xff0000;
|
||||
} else {
|
||||
return 0x00ffff;
|
||||
}
|
||||
}
|
||||
|
||||
function getFields(ticket: Ticket): APIEmbedField[] {
|
||||
return [
|
||||
{
|
||||
name: 'Type',
|
||||
value: ticket.category,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: ticket.status,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Claimed By',
|
||||
value: !ticket.claimant ? '<Unclaimed>' : ticket.claimant,
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async function createEmbedFromTicket(ticket: Ticket): Promise<EmbedBuilder> {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(getTitle(ticket))
|
||||
.setURL(await getURL(ticket))
|
||||
.setDescription(await getDescription(ticket))
|
||||
.setAuthor(getAuthor(ticket))
|
||||
.setColor(getColor(ticket))
|
||||
.setFields(...getFields(ticket))
|
||||
.setFooter({ text: `Ticket #${ticket.id}` });
|
||||
}
|
||||
|
||||
async function getButtons(ticket: Ticket): Promise<ActionRowBuilder<ButtonBuilder>> {
|
||||
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||
|
||||
const primaryButton = new ButtonBuilder()
|
||||
.setStyle(ButtonStyle.Link);
|
||||
|
||||
let skipPrimary = false;
|
||||
|
||||
if (ticket.category == 'blip') {
|
||||
primaryButton
|
||||
.setLabel('Open Blip')
|
||||
.setURL(`${config.E621_BASE_URL}/blips/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'comment') {
|
||||
primaryButton
|
||||
.setLabel('Open Comment')
|
||||
.setURL(`${config.E621_BASE_URL}/comments/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'dmail') {
|
||||
primaryButton
|
||||
.setLabel('Open DMail')
|
||||
.setURL(`${config.E621_BASE_URL}/dmails/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'forum') {
|
||||
primaryButton
|
||||
.setLabel('Open Forum Post')
|
||||
.setURL(`${config.E621_BASE_URL}/forum_posts/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'pool') {
|
||||
primaryButton
|
||||
.setLabel('Open Pool')
|
||||
.setURL(`${config.E621_BASE_URL}/pools/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'post') {
|
||||
const post = await getE621Post(ticket.target_id);
|
||||
|
||||
if (post && spoilerOrBlacklist(post).action == PostAction.Blacklist) skipPrimary = true;
|
||||
else {
|
||||
primaryButton
|
||||
.setLabel('Open Post')
|
||||
.setURL(`${config.E621_BASE_URL}/posts/${ticket.target_id}`);
|
||||
}
|
||||
} else if (ticket.category == 'set') {
|
||||
primaryButton
|
||||
.setLabel('Open Set')
|
||||
.setURL(`${config.E621_BASE_URL}/post_sets/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'user') {
|
||||
primaryButton
|
||||
.setLabel('Open User')
|
||||
.setURL(`${config.E621_BASE_URL}/users/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'wiki') {
|
||||
primaryButton
|
||||
.setLabel('Open Wiki')
|
||||
.setURL(`${config.E621_BASE_URL}/wikis/${ticket.target_id}`);
|
||||
} else {
|
||||
console.error('Unknown ticket type:');
|
||||
console.error(JSON.stringify(ticket, null, 2));
|
||||
skipPrimary = true;
|
||||
}
|
||||
|
||||
if (!skipPrimary) row.addComponents(primaryButton);
|
||||
|
||||
if (ticket.category == 'blip' || ticket.category == 'comment' || ticket.category == 'dmail' || ticket.category == 'forum') {
|
||||
const button = new ButtonBuilder()
|
||||
.setLabel('Open Target User')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`${config.E621_BASE_URL}/users/${ticket.accused_id}`);
|
||||
|
||||
row.addComponents(button);
|
||||
} else if (ticket.category == 'post') {
|
||||
const user = await getE621User(ticket.target!);
|
||||
|
||||
if (user) {
|
||||
const button = new ButtonBuilder()
|
||||
.setLabel('Open Target User')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`${config.E621_BASE_URL}/users/${user.id}`);
|
||||
|
||||
row.addComponents(button);
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
async function sendTicketAlerts(ticket: Ticket, channel: SendableChannels) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.admin_role_id) return;
|
||||
|
||||
const usersToMention: string[] = [];
|
||||
const rolesToMention: string[] = [];
|
||||
let content = '';
|
||||
|
||||
await Database.getAllTicketPhrases((ticketPhrase: TicketPhrase) => {
|
||||
const { alert, match } = shouldAlert(ticketPhrase, ticket);
|
||||
if (alert) {
|
||||
const mention = ticketPhrase.user_id == 'admin' ? `<@&${guildSettings.admin_role_id!}>` : `<@${ticketPhrase.user_id}>`;
|
||||
|
||||
if (ticketPhrase.user_id == 'admin' && !rolesToMention.includes(guildSettings.admin_role_id!)) {
|
||||
rolesToMention.push(guildSettings.admin_role_id!);
|
||||
} else if (!usersToMention.includes(ticketPhrase.user_id)) {
|
||||
usersToMention.push(ticketPhrase.user_id);
|
||||
}
|
||||
|
||||
content += `${mention}: ${match}\n`;
|
||||
}
|
||||
});
|
||||
|
||||
if (content.length == 0) return;
|
||||
|
||||
await channel.send({
|
||||
content,
|
||||
allowedMentions: {
|
||||
users: usersToMention,
|
||||
roles: rolesToMention
|
||||
}
|
||||
});
|
||||
import { APIEmbedField, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedAuthorOptions, EmbedBuilder, SendableChannels } from 'discord.js';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import { Ticket, TicketPhrase, TicketUpdate } from '../types';
|
||||
import { PostAction, getE621Post, getE621User, spoilerOrBlacklist } from './e621-utils';
|
||||
import { blipIDRegex, commentIDRegex, forumTopicIDRegex, poolIDRegex, postIDRegex, recordIDRegex, searchLinkRegex, setIDRegex, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from './message-matcher-regex';
|
||||
import { humanizeCapitalization } from './string-utils';
|
||||
import { shouldAlert } from './ticket-utils';
|
||||
|
||||
// TODO: Condense this and the message event handler regex array.
|
||||
const linkReplacers = [
|
||||
{
|
||||
regex: blipIDRegex,
|
||||
replacement: '/blips/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: commentIDRegex,
|
||||
replacement: '/comments/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: forumTopicIDRegex,
|
||||
replacement: '/forum_topics/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: poolIDRegex,
|
||||
replacement: '/pools/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: postIDRegex,
|
||||
tester: async (postId: string, before: string, after: string) => {
|
||||
const post = await getE621Post(postId);
|
||||
if (!post) return { allowed: true, before, after };
|
||||
const allowed = spoilerOrBlacklist(post).action != PostAction.Blacklist;
|
||||
|
||||
return { allowed, before, after };
|
||||
},
|
||||
replacement: '/posts/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: recordIDRegex,
|
||||
replacement: '/user_feedbacks/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: searchLinkRegex,
|
||||
replacement: '/posts?tags={match}',
|
||||
encodeURI: true
|
||||
},
|
||||
{
|
||||
regex: setIDRegex,
|
||||
replacement: '/post_sets/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: takedownIDRegex,
|
||||
replacement: '/takedowns/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: ticketIDRegex,
|
||||
replacement: '/tickets/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: userIDRegex,
|
||||
replacement: '/users/{match}',
|
||||
encodeURI: false
|
||||
},
|
||||
{
|
||||
regex: wikiLinkRegex,
|
||||
replacement: '/wiki_pages/{match}',
|
||||
encodeURI: true
|
||||
}
|
||||
];
|
||||
|
||||
const urlRegex = new RegExp('"((?:[\\S]| )+?)":\\[?((?:https?:\\/\\/[\\w\\d.\\/?=#&%]+)|\\/[\\w\\d.\\/?=#\\[\\]]+)\\]?', 'gi');
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
export async function ticketUpdateHandler(client: Client, update: string) {
|
||||
const data: TicketUpdate = JSON.parse(update);
|
||||
|
||||
if (data.action == 'create') {
|
||||
postTicket(client, data);
|
||||
} else {
|
||||
updateTicket(client, data);
|
||||
}
|
||||
}
|
||||
|
||||
async function postTicket(client: Client, data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const ticket = data.ticket;
|
||||
|
||||
const embed = await createEmbedFromTicket(ticket);
|
||||
|
||||
const row = await getButtons(ticket);
|
||||
|
||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||
|
||||
await Database.putTicket(ticket.id, message.id);
|
||||
|
||||
sendTicketAlerts(ticket, channel);
|
||||
}
|
||||
|
||||
async function updateTicket(client: Client, data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await client.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const messageId = await Database.getTicketMessageId(data.ticket.id);
|
||||
|
||||
if (!messageId) return postTicket(client, data);
|
||||
|
||||
const message = await channel.messages.fetch(messageId);
|
||||
const embed = await createEmbedFromTicket(data.ticket);
|
||||
|
||||
if (!message || message.author.id != config.DISCORD_CLIENT_ID) {
|
||||
const newMessage = await channel.send({ embeds: [embed] });
|
||||
await Database.removeTicket(data.ticket.id);
|
||||
await Database.putTicket(data.ticket.id, newMessage.id);
|
||||
} else {
|
||||
await message.edit({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
function getTitle(ticket: Ticket): string {
|
||||
if (!ticket.target) return `${humanizeCapitalization(ticket.category)} report by ${ticket.user}`;
|
||||
|
||||
switch (ticket.category) {
|
||||
case 'blip':
|
||||
return `Blip by ${ticket.target}`;
|
||||
case 'comment':
|
||||
return `Comment by ${ticket.target}`;
|
||||
case 'dmail':
|
||||
return `DMail sent by ${ticket.target}`;
|
||||
case 'forum':
|
||||
return `Forum post by ${ticket.target}`;
|
||||
case 'pool':
|
||||
return `Pool ${ticket.target}`;
|
||||
case 'post':
|
||||
return `Post uploaded by ${ticket.target}`;
|
||||
case 'set':
|
||||
return `Wow, a rare set report! ${ticket.target}`;
|
||||
case 'user':
|
||||
return `User ${ticket.target}`;
|
||||
case 'wiki':
|
||||
return `Wiki page ${ticket.target}`;
|
||||
default:
|
||||
return 'Uknown ticket category';
|
||||
}
|
||||
}
|
||||
|
||||
function getURL(ticket: Ticket): string {
|
||||
return `${config.E621_BASE_URL}/tickets/${ticket.id}`;
|
||||
}
|
||||
|
||||
async function getLinks(input: string, limit: number = Number.MAX_SAFE_INTEGER): Promise<string> {
|
||||
const length = input.length;
|
||||
|
||||
const replacedIndexes: { start: number, end: number }[] = [];
|
||||
const checks: Promise<{ allowed: boolean, before: string, after: string }>[] = [];
|
||||
|
||||
for (const replacer of linkReplacers) {
|
||||
input = input.replaceAll(replacer.regex, (match, group1) => {
|
||||
const replaced = `[${match}](${config.E621_BASE_URL}${(replacer.replacement).replace('{match}', replacer.encodeURI ? encodeURIComponent(group1) : group1)})`;
|
||||
if (replacer.tester) checks.push(replacer.tester(group1, match, replaced));
|
||||
const start = input.indexOf(match);
|
||||
replacedIndexes.push({ start, end: start + replaced.length });
|
||||
|
||||
return replaced;
|
||||
});
|
||||
}
|
||||
|
||||
input = input.replaceAll(urlRegex, (match, group1, group2) => {
|
||||
const replaced = group2.startsWith('/') ? `[${group1}](${config.E621_BASE_URL}${group2})` : `[${group1}](${group2})`;
|
||||
const start = input.indexOf(match);
|
||||
replacedIndexes.push({ start, end: start + replaced.length });
|
||||
return replaced;
|
||||
});
|
||||
|
||||
const values = await Promise.all(checks);
|
||||
|
||||
for (const check of values) {
|
||||
if (!check.allowed) {
|
||||
input = input.replace(check.after, check.before);
|
||||
}
|
||||
}
|
||||
|
||||
if (length > limit) {
|
||||
for (const replacedIndex of replacedIndexes) {
|
||||
if (replacedIndex.start < limit && replacedIndex.end >= limit) {
|
||||
return input.substring(0, replacedIndex.end) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
return input.substring(0, limit) + '...';
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
async function getDescription(ticket: Ticket): Promise<string> {
|
||||
return ticket.reason.length <= MAX_DESCRIPTION_LENGTH ? await getLinks(ticket.reason) : await getLinks(ticket.reason, MAX_DESCRIPTION_LENGTH);
|
||||
}
|
||||
|
||||
function getAuthor(ticket: Ticket): EmbedAuthorOptions {
|
||||
return {
|
||||
url: `${config.E621_BASE_URL}/users/${ticket.user_id}`,
|
||||
name: ticket.user
|
||||
};
|
||||
}
|
||||
|
||||
function getColor(ticket: Ticket): number {
|
||||
if (!ticket.claimant) {
|
||||
return 0xff0000;
|
||||
} else {
|
||||
return 0x00ffff;
|
||||
}
|
||||
}
|
||||
|
||||
function getFields(ticket: Ticket): APIEmbedField[] {
|
||||
return [
|
||||
{
|
||||
name: 'Type',
|
||||
value: ticket.category,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: ticket.status,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Claimed By',
|
||||
value: !ticket.claimant ? '<Unclaimed>' : ticket.claimant,
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async function createEmbedFromTicket(ticket: Ticket): Promise<EmbedBuilder> {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(getTitle(ticket))
|
||||
.setURL(await getURL(ticket))
|
||||
.setDescription(await getDescription(ticket))
|
||||
.setAuthor(getAuthor(ticket))
|
||||
.setColor(getColor(ticket))
|
||||
.setFields(...getFields(ticket))
|
||||
.setFooter({ text: `Ticket #${ticket.id}` });
|
||||
}
|
||||
|
||||
async function getButtons(ticket: Ticket): Promise<ActionRowBuilder<ButtonBuilder>> {
|
||||
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||
|
||||
const primaryButton = new ButtonBuilder()
|
||||
.setStyle(ButtonStyle.Link);
|
||||
|
||||
let skipPrimary = false;
|
||||
|
||||
if (ticket.category == 'blip') {
|
||||
primaryButton
|
||||
.setLabel('Open Blip')
|
||||
.setURL(`${config.E621_BASE_URL}/blips/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'comment') {
|
||||
primaryButton
|
||||
.setLabel('Open Comment')
|
||||
.setURL(`${config.E621_BASE_URL}/comments/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'dmail') {
|
||||
primaryButton
|
||||
.setLabel('Open DMail')
|
||||
.setURL(`${config.E621_BASE_URL}/dmails/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'forum') {
|
||||
primaryButton
|
||||
.setLabel('Open Forum Post')
|
||||
.setURL(`${config.E621_BASE_URL}/forum_posts/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'pool') {
|
||||
primaryButton
|
||||
.setLabel('Open Pool')
|
||||
.setURL(`${config.E621_BASE_URL}/pools/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'post') {
|
||||
const post = await getE621Post(ticket.target_id);
|
||||
|
||||
if (post && spoilerOrBlacklist(post).action == PostAction.Blacklist) skipPrimary = true;
|
||||
else {
|
||||
primaryButton
|
||||
.setLabel('Open Post')
|
||||
.setURL(`${config.E621_BASE_URL}/posts/${ticket.target_id}`);
|
||||
}
|
||||
} else if (ticket.category == 'set') {
|
||||
primaryButton
|
||||
.setLabel('Open Set')
|
||||
.setURL(`${config.E621_BASE_URL}/post_sets/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'user') {
|
||||
primaryButton
|
||||
.setLabel('Open User')
|
||||
.setURL(`${config.E621_BASE_URL}/users/${ticket.target_id}`);
|
||||
} else if (ticket.category == 'wiki') {
|
||||
primaryButton
|
||||
.setLabel('Open Wiki')
|
||||
.setURL(`${config.E621_BASE_URL}/wikis/${ticket.target_id}`);
|
||||
} else {
|
||||
console.error('Unknown ticket type:');
|
||||
console.error(JSON.stringify(ticket, null, 2));
|
||||
skipPrimary = true;
|
||||
}
|
||||
|
||||
if (!skipPrimary) row.addComponents(primaryButton);
|
||||
|
||||
if (ticket.category == 'blip' || ticket.category == 'comment' || ticket.category == 'dmail' || ticket.category == 'forum') {
|
||||
const button = new ButtonBuilder()
|
||||
.setLabel('Open Target User')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`${config.E621_BASE_URL}/users/${ticket.accused_id}`);
|
||||
|
||||
row.addComponents(button);
|
||||
} else if (ticket.category == 'post') {
|
||||
const user = await getE621User(ticket.target!);
|
||||
|
||||
if (user) {
|
||||
const button = new ButtonBuilder()
|
||||
.setLabel('Open Target User')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`${config.E621_BASE_URL}/users/${user.id}`);
|
||||
|
||||
row.addComponents(button);
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
async function sendTicketAlerts(ticket: Ticket, channel: SendableChannels) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.admin_role_id) return;
|
||||
|
||||
const usersToMention: string[] = [];
|
||||
const rolesToMention: string[] = [];
|
||||
let content = '';
|
||||
|
||||
await Database.getAllTicketPhrases((ticketPhrase: TicketPhrase) => {
|
||||
const { alert, match } = shouldAlert(ticketPhrase, ticket);
|
||||
if (alert) {
|
||||
const mention = ticketPhrase.user_id == 'admin' ? `<@&${guildSettings.admin_role_id!}>` : `<@${ticketPhrase.user_id}>`;
|
||||
|
||||
if (ticketPhrase.user_id == 'admin' && !rolesToMention.includes(guildSettings.admin_role_id!)) {
|
||||
rolesToMention.push(guildSettings.admin_role_id!);
|
||||
} else if (!usersToMention.includes(ticketPhrase.user_id)) {
|
||||
usersToMention.push(ticketPhrase.user_id);
|
||||
}
|
||||
|
||||
content += `${mention}: ${match}\n`;
|
||||
}
|
||||
});
|
||||
|
||||
if (content.length == 0) return;
|
||||
|
||||
await channel.send({
|
||||
content,
|
||||
allowedMentions: {
|
||||
users: usersToMention,
|
||||
roles: rolesToMention
|
||||
}
|
||||
});
|
||||
}
|
||||
+37
-37
@@ -1,38 +1,38 @@
|
||||
import { Ticket, TicketPhrase } from '../types';
|
||||
|
||||
function friendlyPhrase(phrase: string): string {
|
||||
switch (phrase) {
|
||||
case 'underage porn':
|
||||
case 'child porn':
|
||||
case 'cp':
|
||||
return 'Code Red';
|
||||
default:
|
||||
return phrase;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAlert(ticketPhrase: TicketPhrase, ticket: Ticket): { alert: boolean, match?: string } {
|
||||
if (!(ticketPhrase.phrase.startsWith('/') && ticketPhrase.phrase.endsWith('/'))) {
|
||||
if (ticket.reason.toLowerCase().includes(ticketPhrase.phrase.toLowerCase())) {
|
||||
return { alert: true, match: friendlyPhrase(ticketPhrase.phrase.trim()) };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const regex = new RegExp(ticketPhrase.phrase.slice(1, -1), 'i');
|
||||
|
||||
const regexMatch = regex.exec(ticket.reason);
|
||||
|
||||
if (regexMatch) {
|
||||
return { alert: true, match: `${friendlyPhrase(regexMatch[0].trim())} (RegEx match: \`${ticketPhrase.phrase}\`)` };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return { alert: false };
|
||||
}
|
||||
}
|
||||
import { Ticket, TicketPhrase } from '../types';
|
||||
|
||||
function friendlyPhrase(phrase: string): string {
|
||||
switch (phrase) {
|
||||
case 'underage porn':
|
||||
case 'child porn':
|
||||
case 'cp':
|
||||
return 'Code Red';
|
||||
default:
|
||||
return phrase;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAlert(ticketPhrase: TicketPhrase, ticket: Ticket): { alert: boolean, match?: string } {
|
||||
if (!(ticketPhrase.phrase.startsWith('/') && ticketPhrase.phrase.endsWith('/'))) {
|
||||
if (ticket.reason.toLowerCase().includes(ticketPhrase.phrase.toLowerCase())) {
|
||||
return { alert: true, match: friendlyPhrase(ticketPhrase.phrase.trim()) };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const regex = new RegExp(ticketPhrase.phrase.slice(1, -1), 'i');
|
||||
|
||||
const regexMatch = regex.exec(ticket.reason);
|
||||
|
||||
if (regexMatch) {
|
||||
return { alert: true, match: `${friendlyPhrase(regexMatch[0].trim())} (RegEx match: \`${ticketPhrase.phrase}\`)` };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return { alert: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
export function wait(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
export function wait(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
+17
-17
@@ -1,18 +1,18 @@
|
||||
import { CommandInteraction, MessageFlags } from 'discord.js';
|
||||
import { getE621Alts } from './alt-utils';
|
||||
import { resolveUser } from './discord-user-utils';
|
||||
|
||||
export async function handleWhoIsInteraction(interaction: CommandInteraction, valueToUse: string, ephemeral = false) {
|
||||
if (ephemeral) await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
else await interaction.deferReply();
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = await resolveUser(interaction.client, valueToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
const content = await getE621Alts(user.id, interaction.guild!);
|
||||
|
||||
interaction.editReply(`<@${user.id}>'s (${user.id}) e621 and discord account(s):\n${content}`);
|
||||
import { CommandInteraction, MessageFlags } from 'discord.js';
|
||||
import { getE621Alts } from './alt-utils';
|
||||
import { resolveUser } from './discord-user-utils';
|
||||
|
||||
export async function handleWhoIsInteraction(interaction: CommandInteraction, valueToUse: string, ephemeral = false) {
|
||||
if (ephemeral) await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
else await interaction.deferReply();
|
||||
|
||||
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||
|
||||
const user = await resolveUser(interaction.client, valueToUse, interaction.guild);
|
||||
|
||||
if (!user) return interaction.editReply('User not found.');
|
||||
|
||||
const content = await getE621Alts(user.id, interaction.guild!);
|
||||
|
||||
interaction.editReply(`<@${user.id}>'s (${user.id}) e621 and discord account(s):\n${content}`);
|
||||
}
|
||||
+302
-302
@@ -1,303 +1,303 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import crypto from 'crypto';
|
||||
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, removeIssueLinks } from '../utils/github-user-utils';
|
||||
import { logDebug } from '../utils/debug-utils';
|
||||
import { AltData, comprehensiveAltLookupFromE621, DiscordOAuth2 } from '../utils';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
username: string;
|
||||
userId: string;
|
||||
oauthState: string;
|
||||
}
|
||||
}
|
||||
const GITHUB_REPO_ID = 169334303;
|
||||
|
||||
const DEV_BASE_URL = `http://localhost:${config.PORT}`;
|
||||
const PROD_BASE_URL = 'https://discord.e621.net';
|
||||
|
||||
const OAUTH_SCOPES = ['identify', 'guilds.join'];
|
||||
|
||||
const PAGE_TEMPLATE = fs.readFileSync(path.join(__dirname, 'templates', 'page.html'), { encoding: 'utf-8' });
|
||||
|
||||
const oauth = new DiscordOAuth2({
|
||||
clientId: config.DISCORD_CLIENT_ID!,
|
||||
clientSecret: config.DISCORD_CLIENT_SECRET!,
|
||||
redirectUri: `${config.DEV_MODE ? DEV_BASE_URL : PROD_BASE_URL}/callback`,
|
||||
clientToken: config.DISCORD_TOKEN!,
|
||||
credentials: Buffer.from(`${config.DISCORD_CLIENT_ID!}:${config.DISCORD_CLIENT_SECRET!}`).toString('base64')
|
||||
});
|
||||
|
||||
const enum JoinResponse {
|
||||
Success = 1,
|
||||
Error = 2,
|
||||
Banned = 3,
|
||||
Underage = 4
|
||||
};
|
||||
|
||||
async function joinGuild(code: string, userId: string, username: string): Promise<JoinResponse> {
|
||||
let tokenResponse;
|
||||
try {
|
||||
if (Number.isNaN(userId)) return JoinResponse.Error;
|
||||
if (!username) return JoinResponse.Error;
|
||||
|
||||
const id = Number(userId);
|
||||
|
||||
tokenResponse = await oauth.getAccessToken(code, OAUTH_SCOPES);
|
||||
|
||||
const user = await oauth.getUser(tokenResponse.access_token);
|
||||
|
||||
if (!user.id || !user.username) {
|
||||
console.error(`Error joining user (${userId}) to discord. User object missing id or username.`);
|
||||
console.error(user);
|
||||
return JoinResponse.Error;
|
||||
}
|
||||
|
||||
await Database.putUser(id, user);
|
||||
|
||||
const alts = await comprehensiveAltLookupFromE621(id, null);
|
||||
|
||||
if (await checkAltsForFullBans([alts])) return JoinResponse.Banned;
|
||||
|
||||
const response = await oauth.addMember({
|
||||
accessToken: tokenResponse.access_token,
|
||||
guildId: config.DISCORD_GUILD_ID!,
|
||||
userId: user.id,
|
||||
nickname: username
|
||||
});
|
||||
|
||||
if (config.DEBUG) console.log(response);
|
||||
|
||||
if (!response) return JoinResponse.Error;
|
||||
} catch (e: any) {
|
||||
if (e.code == 40007) return JoinResponse.Banned;
|
||||
else if (e.code == 20024) return JoinResponse.Underage;
|
||||
|
||||
console.error(`Error joining user (${userId}) to discord:`);
|
||||
console.error(e);
|
||||
return JoinResponse.Error;
|
||||
} finally {
|
||||
if (tokenResponse) await oauth.revokeToken(tokenResponse.access_token);
|
||||
}
|
||||
|
||||
return JoinResponse.Success;
|
||||
}
|
||||
|
||||
async function handleInitial(req: Request, res: Response): Promise<any> {
|
||||
const { username, user_id, time, hash } = req.query;
|
||||
|
||||
if (!username || !user_id || !time || !hash) {
|
||||
return sendBadRequest(res, 'Missing parameters');
|
||||
}
|
||||
|
||||
if (Number.isNaN(time) || Date.now() / 1000 > Number(time)) {
|
||||
return render(res, 403, 'You took too long to authorize the request. Please try again.');
|
||||
}
|
||||
|
||||
const authString = `${username} ${user_id} ${time} ${config.LINK_SECRET}`;
|
||||
|
||||
const digest = crypto.createHash('sha256').update(authString).digest('hex');
|
||||
|
||||
if (hash !== digest) {
|
||||
console.error(`Bad auth: ${hash} ${digest}`);
|
||||
return sendForbidden(res, 'Bad auth');
|
||||
}
|
||||
|
||||
const oauthState = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
const oauthUrl = await oauth.generateOauth2Url({
|
||||
state: oauthState,
|
||||
scope: OAUTH_SCOPES,
|
||||
type: 'code'
|
||||
});
|
||||
|
||||
req.session.username = username as string;
|
||||
req.session.userId = user_id as string;
|
||||
req.session.oauthState = oauthState;
|
||||
|
||||
req.session.save((e) => {
|
||||
if (e) {
|
||||
console.error('Error saving session:');
|
||||
console.error(e);
|
||||
return sendInteralServerError(res);
|
||||
}
|
||||
|
||||
res.redirect(oauthUrl);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCallback(req: Request, res: Response): Promise<any> {
|
||||
if (!req.session.userId || !req.session.username || !req.session.oauthState) {
|
||||
return sendForbidden(res, 'Session details missing');
|
||||
}
|
||||
|
||||
const state = req.query.state as string;
|
||||
|
||||
if (state != req.session.oauthState) {
|
||||
console.error('OAuth state mismatch on discord joining');
|
||||
return sendForbidden(res, 'OAuth state mismatch');
|
||||
}
|
||||
|
||||
const code = req.query.code as string;
|
||||
|
||||
const userId = req.session.userId;
|
||||
const username = req.session.username;
|
||||
|
||||
req.session.destroy((e) => {
|
||||
if (e) console.error(e);
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await joinGuild(code, userId, username);
|
||||
if (response == JoinResponse.Error) {
|
||||
console.error(`Error joining user: ${username} (${userId})`);
|
||||
return sendInteralServerError(res, 'Unable to join user to guild. Retry later. If issue persists, please contact staff.');
|
||||
} else if (response == JoinResponse.Banned) {
|
||||
return sendForbidden(res, 'User is banned.');
|
||||
} else if (response == JoinResponse.Underage) {
|
||||
return sendForbidden(res, 'Discord account flagged as underage by discord.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return sendInteralServerError(res);
|
||||
}
|
||||
|
||||
render(res, 200, 'Success', `You have been added to the server. <a href="https://discord.com/channels/${config.DISCORD_GUILD_ID}">See you there.</a>`);
|
||||
}
|
||||
|
||||
function sendInteralServerError(res: Response, message: string = '') {
|
||||
render(res, 500, 'Internal Server Error', message);
|
||||
}
|
||||
|
||||
function sendForbidden(res: Response, message: string = '') {
|
||||
render(res, 403, 'Forbidden', message);
|
||||
}
|
||||
|
||||
function sendBadRequest(res: Response, message: string = '') {
|
||||
render(res, 400, 'Bad Request', message);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
async function handleGithubRelease(client: Client, req: Request, res: Response): Promise<any> {
|
||||
logDebug('Received github release webhook');
|
||||
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) {
|
||||
console.error('Github release webhook signature mismatch');
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
|
||||
const data = JSON.parse(req.body);
|
||||
logDebug(`Release webhook data:\n${JSON.stringify(data, null, 4)}`);
|
||||
|
||||
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()) {
|
||||
console.error(`Github release channel ${channel ? 'sendable' : 'found'}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const date = new Date();
|
||||
|
||||
let message = `## [${months[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}](<${data.release.html_url}>)\n\n${await fixPings(removeIssueLinks(data.release.body))}`;
|
||||
|
||||
logDebug('Sending github release message');
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 2000;
|
||||
const ADDITIONAL_PART = '...\n\nYou may view the full changelog on github.';
|
||||
|
||||
if (message.length > MAX_MESSAGE_LENGTH) {
|
||||
const splitMessage = message.split('\n');
|
||||
|
||||
message = '';
|
||||
|
||||
for (const part of splitMessage) {
|
||||
if (message.length + part.length + 1 >= MAX_MESSAGE_LENGTH - ADDITIONAL_PART.length) break;
|
||||
|
||||
message += `${part}\n`;
|
||||
}
|
||||
|
||||
message += ADDITIONAL_PART;
|
||||
}
|
||||
|
||||
const sentMessage = await channel.send(message);
|
||||
await sentMessage.startThread({ name: data.release.tag_name });
|
||||
|
||||
logDebug('Github webhook processed');
|
||||
}
|
||||
|
||||
async function checkAltsForFullBans(altData: AltData[]): Promise<boolean> {
|
||||
for (const data of altData) {
|
||||
if (data.type == 'discord') {
|
||||
try {
|
||||
const banData = await Database.getBan(data.thisId as string);
|
||||
if (banData?.full_ban) return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await checkAltsForFullBans(data.alts)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function initializeWebserver(client: Client) {
|
||||
const app = express();
|
||||
|
||||
const Store = MemoryStore(session);
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use(session({
|
||||
secret: config.DISCORD_CLIENT_SECRET!,
|
||||
cookie: {
|
||||
secure: !config.DEV_MODE,
|
||||
httpOnly: !config.DEV_MODE,
|
||||
sameSite: false,
|
||||
maxAge: 300000
|
||||
},
|
||||
store: new Store({
|
||||
checkPeriod: 600000,
|
||||
}),
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`Listening on port ${config.PORT}`);
|
||||
});
|
||||
import express, { Request, Response } from 'express';
|
||||
import { config } from '../config';
|
||||
import { Database } from '../shared/Database';
|
||||
import crypto from 'crypto';
|
||||
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, removeIssueLinks } from '../utils/github-user-utils';
|
||||
import { logDebug } from '../utils/debug-utils';
|
||||
import { AltData, comprehensiveAltLookupFromE621, DiscordOAuth2 } from '../utils';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
username: string;
|
||||
userId: string;
|
||||
oauthState: string;
|
||||
}
|
||||
}
|
||||
const GITHUB_REPO_ID = 169334303;
|
||||
|
||||
const DEV_BASE_URL = `http://localhost:${config.PORT}`;
|
||||
const PROD_BASE_URL = 'https://discord.e621.net';
|
||||
|
||||
const OAUTH_SCOPES = ['identify', 'guilds.join'];
|
||||
|
||||
const PAGE_TEMPLATE = fs.readFileSync(path.join(__dirname, 'templates', 'page.html'), { encoding: 'utf-8' });
|
||||
|
||||
const oauth = new DiscordOAuth2({
|
||||
clientId: config.DISCORD_CLIENT_ID!,
|
||||
clientSecret: config.DISCORD_CLIENT_SECRET!,
|
||||
redirectUri: `${config.DEV_MODE ? DEV_BASE_URL : PROD_BASE_URL}/callback`,
|
||||
clientToken: config.DISCORD_TOKEN!,
|
||||
credentials: Buffer.from(`${config.DISCORD_CLIENT_ID!}:${config.DISCORD_CLIENT_SECRET!}`).toString('base64')
|
||||
});
|
||||
|
||||
const enum JoinResponse {
|
||||
Success = 1,
|
||||
Error = 2,
|
||||
Banned = 3,
|
||||
Underage = 4
|
||||
};
|
||||
|
||||
async function joinGuild(code: string, userId: string, username: string): Promise<JoinResponse> {
|
||||
let tokenResponse;
|
||||
try {
|
||||
if (Number.isNaN(userId)) return JoinResponse.Error;
|
||||
if (!username) return JoinResponse.Error;
|
||||
|
||||
const id = Number(userId);
|
||||
|
||||
tokenResponse = await oauth.getAccessToken(code, OAUTH_SCOPES);
|
||||
|
||||
const user = await oauth.getUser(tokenResponse.access_token);
|
||||
|
||||
if (!user.id || !user.username) {
|
||||
console.error(`Error joining user (${userId}) to discord. User object missing id or username.`);
|
||||
console.error(user);
|
||||
return JoinResponse.Error;
|
||||
}
|
||||
|
||||
await Database.putUser(id, user);
|
||||
|
||||
const alts = await comprehensiveAltLookupFromE621(id, null);
|
||||
|
||||
if (await checkAltsForFullBans([alts])) return JoinResponse.Banned;
|
||||
|
||||
const response = await oauth.addMember({
|
||||
accessToken: tokenResponse.access_token,
|
||||
guildId: config.DISCORD_GUILD_ID!,
|
||||
userId: user.id,
|
||||
nickname: username
|
||||
});
|
||||
|
||||
if (config.DEBUG) console.log(response);
|
||||
|
||||
if (!response) return JoinResponse.Error;
|
||||
} catch (e: any) {
|
||||
if (e.code == 40007) return JoinResponse.Banned;
|
||||
else if (e.code == 20024) return JoinResponse.Underage;
|
||||
|
||||
console.error(`Error joining user (${userId}) to discord:`);
|
||||
console.error(e);
|
||||
return JoinResponse.Error;
|
||||
} finally {
|
||||
if (tokenResponse) await oauth.revokeToken(tokenResponse.access_token);
|
||||
}
|
||||
|
||||
return JoinResponse.Success;
|
||||
}
|
||||
|
||||
async function handleInitial(req: Request, res: Response): Promise<any> {
|
||||
const { username, user_id, time, hash } = req.query;
|
||||
|
||||
if (!username || !user_id || !time || !hash) {
|
||||
return sendBadRequest(res, 'Missing parameters');
|
||||
}
|
||||
|
||||
if (Number.isNaN(time) || Date.now() / 1000 > Number(time)) {
|
||||
return render(res, 403, 'You took too long to authorize the request. Please try again.');
|
||||
}
|
||||
|
||||
const authString = `${username} ${user_id} ${time} ${config.LINK_SECRET}`;
|
||||
|
||||
const digest = crypto.createHash('sha256').update(authString).digest('hex');
|
||||
|
||||
if (hash !== digest) {
|
||||
console.error(`Bad auth: ${hash} ${digest}`);
|
||||
return sendForbidden(res, 'Bad auth');
|
||||
}
|
||||
|
||||
const oauthState = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
const oauthUrl = await oauth.generateOauth2Url({
|
||||
state: oauthState,
|
||||
scope: OAUTH_SCOPES,
|
||||
type: 'code'
|
||||
});
|
||||
|
||||
req.session.username = username as string;
|
||||
req.session.userId = user_id as string;
|
||||
req.session.oauthState = oauthState;
|
||||
|
||||
req.session.save((e) => {
|
||||
if (e) {
|
||||
console.error('Error saving session:');
|
||||
console.error(e);
|
||||
return sendInteralServerError(res);
|
||||
}
|
||||
|
||||
res.redirect(oauthUrl);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCallback(req: Request, res: Response): Promise<any> {
|
||||
if (!req.session.userId || !req.session.username || !req.session.oauthState) {
|
||||
return sendForbidden(res, 'Session details missing');
|
||||
}
|
||||
|
||||
const state = req.query.state as string;
|
||||
|
||||
if (state != req.session.oauthState) {
|
||||
console.error('OAuth state mismatch on discord joining');
|
||||
return sendForbidden(res, 'OAuth state mismatch');
|
||||
}
|
||||
|
||||
const code = req.query.code as string;
|
||||
|
||||
const userId = req.session.userId;
|
||||
const username = req.session.username;
|
||||
|
||||
req.session.destroy((e) => {
|
||||
if (e) console.error(e);
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await joinGuild(code, userId, username);
|
||||
if (response == JoinResponse.Error) {
|
||||
console.error(`Error joining user: ${username} (${userId})`);
|
||||
return sendInteralServerError(res, 'Unable to join user to guild. Retry later. If issue persists, please contact staff.');
|
||||
} else if (response == JoinResponse.Banned) {
|
||||
return sendForbidden(res, 'User is banned.');
|
||||
} else if (response == JoinResponse.Underage) {
|
||||
return sendForbidden(res, 'Discord account flagged as underage by discord.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return sendInteralServerError(res);
|
||||
}
|
||||
|
||||
render(res, 200, 'Success', `You have been added to the server. <a href="https://discord.com/channels/${config.DISCORD_GUILD_ID}">See you there.</a>`);
|
||||
}
|
||||
|
||||
function sendInteralServerError(res: Response, message: string = '') {
|
||||
render(res, 500, 'Internal Server Error', message);
|
||||
}
|
||||
|
||||
function sendForbidden(res: Response, message: string = '') {
|
||||
render(res, 403, 'Forbidden', message);
|
||||
}
|
||||
|
||||
function sendBadRequest(res: Response, message: string = '') {
|
||||
render(res, 400, 'Bad Request', message);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
async function handleGithubRelease(client: Client, req: Request, res: Response): Promise<any> {
|
||||
logDebug('Received github release webhook');
|
||||
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) {
|
||||
console.error('Github release webhook signature mismatch');
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
|
||||
const data = JSON.parse(req.body);
|
||||
logDebug(`Release webhook data:\n${JSON.stringify(data, null, 4)}`);
|
||||
|
||||
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()) {
|
||||
console.error(`Github release channel ${channel ? 'sendable' : 'found'}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const date = new Date();
|
||||
|
||||
let message = `## [${months[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}](<${data.release.html_url}>)\n\n${await fixPings(removeIssueLinks(data.release.body))}`;
|
||||
|
||||
logDebug('Sending github release message');
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 2000;
|
||||
const ADDITIONAL_PART = '...\n\nYou may view the full changelog on github.';
|
||||
|
||||
if (message.length > MAX_MESSAGE_LENGTH) {
|
||||
const splitMessage = message.split('\n');
|
||||
|
||||
message = '';
|
||||
|
||||
for (const part of splitMessage) {
|
||||
if (message.length + part.length + 1 >= MAX_MESSAGE_LENGTH - ADDITIONAL_PART.length) break;
|
||||
|
||||
message += `${part}\n`;
|
||||
}
|
||||
|
||||
message += ADDITIONAL_PART;
|
||||
}
|
||||
|
||||
const sentMessage = await channel.send(message);
|
||||
await sentMessage.startThread({ name: data.release.tag_name });
|
||||
|
||||
logDebug('Github webhook processed');
|
||||
}
|
||||
|
||||
async function checkAltsForFullBans(altData: AltData[]): Promise<boolean> {
|
||||
for (const data of altData) {
|
||||
if (data.type == 'discord') {
|
||||
try {
|
||||
const banData = await Database.getBan(data.thisId as string);
|
||||
if (banData?.full_ban) return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await checkAltsForFullBans(data.alts)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function initializeWebserver(client: Client) {
|
||||
const app = express();
|
||||
|
||||
const Store = MemoryStore(session);
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use(session({
|
||||
secret: config.DISCORD_CLIENT_SECRET!,
|
||||
cookie: {
|
||||
secure: !config.DEV_MODE,
|
||||
httpOnly: !config.DEV_MODE,
|
||||
sameSite: false,
|
||||
maxAge: 300000
|
||||
},
|
||||
store: new Store({
|
||||
checkPeriod: 600000,
|
||||
}),
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`Listening on port ${config.PORT}`);
|
||||
});
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #012e56;
|
||||
color: #fff;
|
||||
font-family: Verdana, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #b4c7d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #e9f2fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #012e56;
|
||||
color: #fff;
|
||||
font-family: Verdana, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #b4c7d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #e9f2fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
+21
-21
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ES2021.String"
|
||||
]
|
||||
},
|
||||
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ES2021.String"
|
||||
]
|
||||
},
|
||||
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user