Merge branch 'master' into pr/5
This commit is contained in:
@@ -22,6 +22,12 @@ export default {
|
|||||||
.setDescription('Set the ticket logs channel.')
|
.setDescription('Set the ticket logs channel.')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
)
|
)
|
||||||
|
.addChannelOption(option =>
|
||||||
|
option
|
||||||
|
.setName('appeals-channel')
|
||||||
|
.setDescription('Set the appeal logs channel.')
|
||||||
|
.setRequired(false)
|
||||||
|
)
|
||||||
.addChannelOption(option =>
|
.addChannelOption(option =>
|
||||||
option
|
option
|
||||||
.setName('event-logs-channel')
|
.setName('event-logs-channel')
|
||||||
@@ -139,6 +145,14 @@ export default {
|
|||||||
response.push(`**tickets_channel_id** has been set to: ${ticketsChannel}.`);
|
response.push(`**tickets_channel_id** has been set to: ${ticketsChannel}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appealsChannel = interaction.options.getChannel('appeals-channel');
|
||||||
|
|
||||||
|
if (appealsChannel) {
|
||||||
|
await Database.setGuildAppealsLogsChannelId(interaction.guildId!, appealsChannel.id);
|
||||||
|
|
||||||
|
response += `Appeals logs channel set to ${appealsChannel}.\n`;
|
||||||
|
}
|
||||||
|
|
||||||
const eventLogsChannel = interaction.options.getChannel('event-logs-channel');
|
const eventLogsChannel = interaction.options.getChannel('event-logs-channel');
|
||||||
if (eventLogsChannel) {
|
if (eventLogsChannel) {
|
||||||
await Database.updateGuildSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id);
|
await Database.updateGuildSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Up
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
ALTER TABLE settings ADD appeals_channel_id TEXT;
|
||||||
|
|
||||||
|
CREATE TABLE appeals (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
message_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Down
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
ALTER TABLE settings DROP COLUMN appeals_channel_id;
|
||||||
|
|
||||||
|
DROP TABLE appeals;
|
||||||
+33
-1
@@ -1,5 +1,8 @@
|
|||||||
import sqlite3 from 'sqlite3';
|
import path from 'path';
|
||||||
import { open, Database as SqliteDatabase } from 'sqlite';
|
import { open, Database as SqliteDatabase } from 'sqlite';
|
||||||
|
import sqlite3 from 'sqlite3';
|
||||||
|
import { Message } from '../events';
|
||||||
|
import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, TicketMessage, TicketPhrase } from '../types';
|
||||||
import { serializeMessage, wait } from '../utils';
|
import { serializeMessage, wait } from '../utils';
|
||||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket, GuildSetting } from '../types';
|
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket, GuildSetting } from '../types';
|
||||||
import { Message } from '../events';
|
import { Message } from '../events';
|
||||||
@@ -128,6 +131,8 @@ export class Database {
|
|||||||
console.log('SQLite database opened');
|
console.log('SQLite database opened');
|
||||||
|
|
||||||
await Database.ensure();
|
await Database.ensure();
|
||||||
|
|
||||||
|
await Database.migrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ensure() {
|
private static async ensure() {
|
||||||
@@ -135,6 +140,16 @@ export class Database {
|
|||||||
console.log('SQLite database ensured');
|
console.log('SQLite database ensured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async migrate() {
|
||||||
|
console.log('Starting database migrations');
|
||||||
|
|
||||||
|
await Database.db.migrate({
|
||||||
|
migrationsPath: path.join(__dirname, '..', 'migrations')
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Database migrations ran');
|
||||||
|
}
|
||||||
|
|
||||||
//#region WHOIS
|
//#region WHOIS
|
||||||
|
|
||||||
static async getE621Ids(discordId: string): Promise<number[]> {
|
static async getE621Ids(discordId: string): Promise<number[]> {
|
||||||
@@ -306,6 +321,23 @@ export class Database {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Appeals
|
||||||
|
|
||||||
|
static async putAppeal(appealId: number, messageId: string) {
|
||||||
|
await Database.db.run('INSERT INTO appeals(id, message_id) VALUES (?, ?)', appealId, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeAppeal(appealId: number) {
|
||||||
|
await Database.db.run('DELETE from appeals WHERE id = ?', appealId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAppealMessageId(appealId: number): Promise<string | undefined> {
|
||||||
|
const appeal = await Database.db.get<Pick<AppealMessage, 'message_id'>>('SELECT message_id FROM appeals WHERE id = ?', appealId);
|
||||||
|
return appeal?.message_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Notes
|
//#region Notes
|
||||||
|
|
||||||
static async putNote(userId: string, reason: string, modId: string) {
|
static async putNote(userId: string, reason: string, modId: string) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createClient, SocketClosedUnexpectedlyError } from '@redis/client';
|
import { createClient, SocketClosedUnexpectedlyError } from '@redis/client';
|
||||||
import { Client } from 'discord.js';
|
import { Client } from 'discord.js';
|
||||||
import { banUpdateHandler, ticketUpdateHandler } from '../utils';
|
import { banUpdateHandler, ticketUpdateHandler, appealUpdateHandler } from '../utils';
|
||||||
|
|
||||||
let discordClient: Client;
|
let discordClient: Client;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export async function openRedisClient(url: string, discClient: Client) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.once('connect', () => {
|
client.once('connect', () => {
|
||||||
client.subscribe(['ticket_updates', 'ban_updates'], updateHandler);
|
client.subscribe(['ticket_updates', 'ban_updates', 'appeal_updates'], updateHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.connect();
|
client.connect();
|
||||||
@@ -46,5 +46,7 @@ function updateHandler(data: string, channel: string) {
|
|||||||
return ticketUpdateHandler(discordClient, data);
|
return ticketUpdateHandler(discordClient, data);
|
||||||
case 'ban_updates':
|
case 'ban_updates':
|
||||||
return banUpdateHandler(discordClient, data);
|
return banUpdateHandler(discordClient, data);
|
||||||
|
case 'appeal_updates':
|
||||||
|
return appealUpdateHandler(discordClient, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+7
-1
@@ -25,7 +25,8 @@ export type GuildSettings = {
|
|||||||
link_skip_channels?: string
|
link_skip_channels?: string
|
||||||
github_release_channel?: string
|
github_release_channel?: string
|
||||||
moderator_channel_id?: string
|
moderator_channel_id?: string
|
||||||
private_help_channel_id?: string
|
private_help_channel_id?: string,
|
||||||
|
appeals_channel_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GuildSetting = Exclude<keyof GuildSettings, 'guild_id'>
|
type GuildSetting = Exclude<keyof GuildSettings, 'guild_id'>
|
||||||
@@ -36,6 +37,11 @@ export type TicketMessage = {
|
|||||||
message_id: string
|
message_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AppealMessage = {
|
||||||
|
id: number
|
||||||
|
message_id: string
|
||||||
|
}
|
||||||
|
|
||||||
export type TicketPhrase = {
|
export type TicketPhrase = {
|
||||||
id: number
|
id: number
|
||||||
user_id: string
|
user_id: string
|
||||||
|
|||||||
Vendored
+18
@@ -142,6 +142,24 @@ export type BanUpdate = {
|
|||||||
ban: Ban
|
ban: Ban
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Appeal = {
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
user: string
|
||||||
|
claimant: string | null
|
||||||
|
target?: string
|
||||||
|
accused_id: number
|
||||||
|
target_id: number
|
||||||
|
status: 'pending' | 'partial' | 'approved' | 'rejected'
|
||||||
|
category: 'flag'
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppealUpdate = {
|
||||||
|
action: 'claim' | 'create' | 'unclaim' | 'update'
|
||||||
|
appeal: Appeal
|
||||||
|
};
|
||||||
|
|
||||||
export type RecordCategory = 'positive' | 'negative' | 'neutral'
|
export type RecordCategory = 'positive' | 'negative' | 'neutral'
|
||||||
|
|
||||||
export type Record = {
|
export type Record = {
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder } from 'discord.js';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { Appeal, AppealUpdate } from '../types';
|
||||||
|
import { getAuthor, getColor, getDescription, getFields } from './event-utils';
|
||||||
|
import { humanizeCapitalization } from './string-utils';
|
||||||
|
import { Database } from '../shared/Database';
|
||||||
|
|
||||||
|
export async function appealUpdateHandler(client: Client, update: string) {
|
||||||
|
const data: AppealUpdate = JSON.parse(update);
|
||||||
|
|
||||||
|
if (data.action == 'create') {
|
||||||
|
postAppeal(client, data);
|
||||||
|
} else {
|
||||||
|
updateAppeal(client, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postAppeal(client: Client, data: AppealUpdate) {
|
||||||
|
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||||
|
|
||||||
|
if (!guildSettings || !guildSettings.appeals_channel_id) return;
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(guildSettings.appeals_channel_id);
|
||||||
|
|
||||||
|
if (!channel || !channel.isSendable()) return;
|
||||||
|
|
||||||
|
const appeal = data.appeal;
|
||||||
|
|
||||||
|
const embed = await createEmbedFromAppeal(appeal);
|
||||||
|
|
||||||
|
const row = await getButtons(appeal);
|
||||||
|
|
||||||
|
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
|
await Database.putAppeal(appeal.id, message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAppeal(client: Client, data: AppealUpdate) {
|
||||||
|
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||||
|
|
||||||
|
if (!guildSettings || !guildSettings.appeals_channel_id) return;
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(guildSettings.appeals_channel_id);
|
||||||
|
|
||||||
|
if (!channel || !channel.isSendable()) return;
|
||||||
|
|
||||||
|
const messageId = await Database.getAppealMessageId(data.appeal.id);
|
||||||
|
|
||||||
|
if (!messageId) return postAppeal(client, data);
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(messageId);
|
||||||
|
const embed = await createEmbedFromAppeal(data.appeal);
|
||||||
|
|
||||||
|
if (!message || message.author.id != config.DISCORD_CLIENT_ID) {
|
||||||
|
const newMessage = await channel.send({ embeds: [embed] });
|
||||||
|
await Database.removeAppeal(data.appeal.id);
|
||||||
|
await Database.putAppeal(data.appeal.id, newMessage.id);
|
||||||
|
} else {
|
||||||
|
await message.edit({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTitle(appeal: Appeal): string {
|
||||||
|
if (!appeal.target) return `${humanizeCapitalization(appeal.category)} appeal from ${appeal.user}`;
|
||||||
|
|
||||||
|
switch (appeal.category) {
|
||||||
|
case 'flag':
|
||||||
|
return `Flag by ${appeal.target}`;
|
||||||
|
default:
|
||||||
|
return 'Uknown appeal category';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getURL(appeal: Appeal): string {
|
||||||
|
return `${config.E621_BASE_URL}/appeals/${appeal.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEmbedFromAppeal(appeal: Appeal): Promise<EmbedBuilder> {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(getTitle(appeal))
|
||||||
|
.setURL(await getURL(appeal))
|
||||||
|
.setDescription(await getDescription(appeal))
|
||||||
|
.setAuthor(getAuthor(appeal))
|
||||||
|
.setColor(getColor(appeal))
|
||||||
|
.setFields(...getFields(appeal))
|
||||||
|
.setFooter({ text: `Appeal #${appeal.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getButtons(appeal: Appeal): Promise<ActionRowBuilder<ButtonBuilder>> {
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||||
|
|
||||||
|
const primaryButton = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Link);
|
||||||
|
|
||||||
|
if (appeal.category == 'flag') {
|
||||||
|
primaryButton
|
||||||
|
.setLabel('Open Flag')
|
||||||
|
.setURL(`${config.E621_BASE_URL}/post_flags/${appeal.target_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.addComponents(primaryButton);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { EmbedAuthorOptions, APIEmbedField } from 'discord.js';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { getE621Post, spoilerOrBlacklist, PostAction } from './e621-utils';
|
||||||
|
import { blipIDRegex, commentIDRegex, forumTopicIDRegex, poolIDRegex, postIDRegex, recordIDRegex, searchLinkRegex, setIDRegex, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from './message-matcher-regex';
|
||||||
|
|
||||||
|
// 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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDescription(data: { reason: string }): Promise<string> {
|
||||||
|
return data.reason.length <= MAX_DESCRIPTION_LENGTH ? await getLinks(data.reason) : await getLinks(data.reason, MAX_DESCRIPTION_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthor(data: { user_id: number, user: string }): EmbedAuthorOptions {
|
||||||
|
return {
|
||||||
|
url: `${config.E621_BASE_URL}/users/${data.user_id}`,
|
||||||
|
name: data.user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColor(data: { claimant: string | null }): number {
|
||||||
|
if (!data.claimant) {
|
||||||
|
return 0xff0000;
|
||||||
|
} else {
|
||||||
|
return 0x00ffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFields(data: { category: string, status: string, claimant: string | null }): APIEmbedField[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Type',
|
||||||
|
value: data.category,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Status',
|
||||||
|
value: data.status,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Claimed By',
|
||||||
|
value: !data.claimant ? '<Unclaimed>' : data.claimant,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './alt-utils';
|
export * from './alt-utils';
|
||||||
|
export * from './appeal-events';
|
||||||
export * from './array-utils';
|
export * from './array-utils';
|
||||||
export * from './audit-log-utils';
|
export * from './audit-log-utils';
|
||||||
export * from './ban-events';
|
export * from './ban-events';
|
||||||
|
|||||||
+2
-161
@@ -1,87 +1,12 @@
|
|||||||
import { APIEmbedField, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedAuthorOptions, EmbedBuilder, SendableChannels } from 'discord.js';
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, SendableChannels } from 'discord.js';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { Database } from '../shared/Database';
|
import { Database } from '../shared/Database';
|
||||||
import { Ticket, TicketPhrase, TicketUpdate } from '../types';
|
import { Ticket, TicketPhrase, TicketUpdate } from '../types';
|
||||||
import { PostAction, getE621Post, getE621User, spoilerOrBlacklist } from './e621-utils';
|
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 { getAuthor, getColor, getDescription, getFields } from './event-utils';
|
||||||
import { humanizeCapitalization } from './string-utils';
|
import { humanizeCapitalization } from './string-utils';
|
||||||
import { shouldAlert } from './ticket-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) {
|
export async function ticketUpdateHandler(client: Client, update: string) {
|
||||||
const data: TicketUpdate = JSON.parse(update);
|
const data: TicketUpdate = JSON.parse(update);
|
||||||
|
|
||||||
@@ -170,90 +95,6 @@ function getURL(ticket: Ticket): string {
|
|||||||
return `${config.E621_BASE_URL}/tickets/${ticket.id}`;
|
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> {
|
async function createEmbedFromTicket(ticket: Ticket): Promise<EmbedBuilder> {
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setTitle(getTitle(ticket))
|
.setTitle(getTitle(ticket))
|
||||||
|
|||||||
Reference in New Issue
Block a user