Initial commit.
This commit is contained in:
@@ -0,0 +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 };
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { AuditLogChange, AuditLogEvent, AuditLogOptionsType, GuildAuditLogsEntry, APIRole, time, TimestampStyles, PermissionsBitField, PermissionsString, Guild } from 'discord.js';
|
||||
import { 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)).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') results.push(formatSnowflake(value as string, TargetType.Channel));
|
||||
|
||||
results.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
return results.join('\n');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatChange(change: AuditLogChange): string | undefined {
|
||||
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);
|
||||
}
|
||||
|
||||
if (change.new !== undefined && change.old === undefined)
|
||||
return `Set ${change.key} to ${change.new}`;
|
||||
|
||||
if (change.new === undefined && change.old !== undefined)
|
||||
return `Set ${change.key} with value ${change.old} to default/null`;
|
||||
|
||||
return `Set ${change.key} from ${change.old} to ${change.new}`;
|
||||
}
|
||||
|
||||
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: https://discord.com/${guild.id}/${data.channel.id}/${data.messageId}`;
|
||||
}
|
||||
@@ -0,0 +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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { createClient } from '@redis/client';
|
||||
import { Database } from '../shared/Database';
|
||||
import { config } from '../config';
|
||||
import { APIEmbedField, Client, EmbedAuthorOptions, EmbedBuilder, SendableChannels, TextBasedChannel } from 'discord.js';
|
||||
import { humanizeCapitalization } from './string-utils';
|
||||
import { Ticket, TicketPhrase, TicketUpdate } from '../types';
|
||||
import { shouldAlert } from './ticket-utils';
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
let discordClient: Client;
|
||||
|
||||
export async function openRedisClient(url: string, discClient: Client) {
|
||||
const client = await createClient({
|
||||
url: `redis://${url}`
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
discordClient = discClient;
|
||||
|
||||
console.log('Connected to redis database');
|
||||
|
||||
await client.subscribe('ticket_updates', updateHandler);
|
||||
}
|
||||
|
||||
async function updateHandler(update: string, channel: string) {
|
||||
const data: TicketUpdate = JSON.parse(update);
|
||||
|
||||
if (data.action == 'create') {
|
||||
postTicket(data);
|
||||
} else {
|
||||
updateTicket(data);
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
function getDescription(ticket: Ticket): string {
|
||||
return ticket.reason.length <= MAX_DESCRIPTION_LENGTH ? ticket.reason : ticket.reason.substring(0, 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
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function createEmbedFromTicket(ticket: Ticket): EmbedBuilder {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(getTitle(ticket))
|
||||
.setURL(getURL(ticket))
|
||||
.setDescription(getDescription(ticket))
|
||||
.setAuthor(getAuthor(ticket))
|
||||
.setColor(getColor(ticket))
|
||||
.setFields(...getFields(ticket));
|
||||
}
|
||||
|
||||
async function postTicket(data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await discordClient.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const ticket = data.ticket;
|
||||
|
||||
const embed = createEmbedFromTicket(ticket);
|
||||
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
|
||||
await Database.putTicket(ticket.id, message.id);
|
||||
|
||||
sendTicketAlerts(ticket, channel);
|
||||
}
|
||||
|
||||
async function updateTicket(data: TicketUpdate) {
|
||||
const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!);
|
||||
|
||||
if (!guildSettings || !guildSettings.tickets_channel_id) return;
|
||||
|
||||
const channel = await discordClient.channels.fetch(guildSettings.tickets_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const messageId = await Database.getTicketMessageId(data.ticket.id);
|
||||
|
||||
if (!messageId) return;
|
||||
|
||||
const message = await channel.messages.fetch(messageId);
|
||||
const embed = createEmbedFromTicket(data.ticket);
|
||||
|
||||
if (!message) {
|
||||
const newMessage = await channel.send({ embeds: [embed] });
|
||||
await Database.putTicket(data.ticket.id, newMessage.id);
|
||||
} else {
|
||||
await message.edit({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { config } from '../config';
|
||||
import { E621Post, E621User } from '../types';
|
||||
|
||||
const BLACKLISTED_TAGS: string[] = [];
|
||||
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
|
||||
|
||||
const USER_AGENT = 'E621DiscordBot';
|
||||
const E621_NAME_URL = `${config.E621_BASE_URL}/users/{idOrName}.json`;
|
||||
const E621_POST_URL = `${config.E621_BASE_URL}/posts/{id}.json`;
|
||||
const E621_MD5_POST_URL = `${config.E621_BASE_URL}/posts.json?md5={md5}`;
|
||||
|
||||
export async function getE621User(idOrName: string | number): Promise<E621User | null> {
|
||||
const res = await fetch(E621_NAME_URL.replace('{idOrName}', idOrName.toString()), {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
return await res.json() as E621User;
|
||||
}
|
||||
|
||||
export async function getE621Post(id: string | number): Promise<E621Post | null> {
|
||||
const res = await fetch(E621_POST_URL.replace('{id}', id.toString()), {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
return (await res.json() as { post: E621Post }).post as E621Post;
|
||||
}
|
||||
|
||||
export async function getE621PostByMd5(md5: string): Promise<E621Post | null> {
|
||||
const res = await fetch(E621_MD5_POST_URL.replace('{md5}', md5), {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
return (await res.json() as { post: E621Post }).post as E621Post;
|
||||
}
|
||||
|
||||
export function hasBlacklistedTags(post: E621Post): boolean {
|
||||
for (const tags of Object.values(post.tags)) {
|
||||
if (tags.some(t => BLACKLISTED_TAGS.includes(t))) return true;
|
||||
if (post.rating != 's' && tags.some(t => BLACKLISTED_NONSAFE_TAGS.includes(t))) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPostUrl(post: E621Post): string {
|
||||
if (post.rating == 's') return `${config.E926_BASE_URL}/post/${post.id}`;
|
||||
return `${config.E621_BASE_URL}/post/${post.id}`;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export * from './array-utils';
|
||||
export * from './audit-log-utils';
|
||||
export * from './commands';
|
||||
export * from './e621-ticket-listener';
|
||||
export * from './e621-utils';
|
||||
export * from './message-logger';
|
||||
export * from './message-utils';
|
||||
export * from './ms-to-human';
|
||||
export * from './refresh-commands';
|
||||
export * from './string-utils';
|
||||
export * from './wait';
|
||||
@@ -0,0 +1,158 @@
|
||||
import { APIEmbedField, Channel, EmbedBuilder, GuildTextBasedChannel, messageLink } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { Message } from '../events';
|
||||
import { deserializeMessagePart, getModifiedAttachments, getModifiedStickers } from './message-utils';
|
||||
import { LoggedMessage } from '../types';
|
||||
|
||||
export async function logEdit(loggedMessage: LoggedMessage, newMessage: Message<true>) {
|
||||
const settings = await Database.getGuildSettings(newMessage.guildId);
|
||||
|
||||
if (!settings || !settings.event_logs_channel_id) return;
|
||||
|
||||
const channel = await newMessage.guild.channels.fetch(settings.event_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, newMessage));
|
||||
fields.push(...getEditEmbeds(loggedMessage, newMessage));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Edited Message')
|
||||
.setColor(0xFFFF00)
|
||||
.setTimestamp(newMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
export async function logDeletion(loggedMessage: LoggedMessage, deletedMessage: Message<true>) {
|
||||
const settings = await Database.getGuildSettings(deletedMessage.guildId);
|
||||
|
||||
if (!settings || !settings.event_logs_channel_id) return;
|
||||
|
||||
const channel = await deletedMessage.guild.channels.fetch(settings.event_logs_channel_id);
|
||||
|
||||
if (!channel || !channel.isSendable()) return;
|
||||
|
||||
const fields: APIEmbedField[] = [];
|
||||
fields.push(...getMainEmbeds(loggedMessage, deletedMessage));
|
||||
fields.push(...getDeletedEmbeds(loggedMessage));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Deleted Message')
|
||||
.setColor(0xFF0000)
|
||||
.setTimestamp(deletedMessage.createdTimestamp)
|
||||
.addFields(...fields);
|
||||
|
||||
channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
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.toString(),
|
||||
inline: true
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getDeletedEmbeds(loggedMessage: LoggedMessage): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
|
||||
if (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>): APIEmbedField[] {
|
||||
const fields: APIEmbedField[] = [];
|
||||
if (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;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Message } from '../events';
|
||||
import { LoggedMessage } from '../types';
|
||||
|
||||
export const ARRAY_SEPARATOR = '$';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +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(', ');
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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 = fs.readdirSync(`${ROOT_DIR}/commands`).filter(file => file.endsWith('.js') || file.endsWith('.ts'));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const command: Command = require(`${ROOT_DIR}/commands/${file}`).default;
|
||||
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));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export function humanizeCapitalization(str: string): string {
|
||||
return str.toLowerCase()
|
||||
.split(' ')
|
||||
.map(s => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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) };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
} else {
|
||||
const regex = new RegExp(ticketPhrase.phrase.substring(1, ticketPhrase.phrase.length - 2), 'i');
|
||||
|
||||
const regexMatch = regex.exec(ticket.reason);
|
||||
|
||||
if (regexMatch) {
|
||||
return { alert: true, match: `${friendlyPhrase(regexMatch[0])} (RegEx match: \`${ticketPhrase.phrase}\`)` };
|
||||
} else {
|
||||
return { alert: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function wait(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
Reference in New Issue
Block a user