Merge branch 'master' into db-rewrite
This commit is contained in:
+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, '');
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user