Add record command
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
import { ApplicationIntegrationType, BitFieldResolvable, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { channelIsInStaffCategory, handleWhoIsInteraction } from '../utils';
|
||||||
|
import { getRecordMessageFromDiscordId } from '../utils/record-utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'records',
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('records')
|
||||||
|
.setDescription("Get a user's on-site records.")
|
||||||
|
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||||
|
.setContexts(InteractionContextType.Guild)
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||||
|
.addUserOption(option =>
|
||||||
|
option
|
||||||
|
.setName('user')
|
||||||
|
.setDescription('The discord user to find the e621 user of.')
|
||||||
|
.setRequired(false)
|
||||||
|
)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option
|
||||||
|
.setName('id')
|
||||||
|
.setDescription('The discord user id to find the e621 user of.')
|
||||||
|
.setRequired(false)
|
||||||
|
),
|
||||||
|
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||||
|
const isStaffChannel = await channelIsInStaffCategory(interaction.channel as GuildBasedChannel);
|
||||||
|
|
||||||
|
if (isStaffChannel) await interaction.deferReply();
|
||||||
|
else await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||||
|
|
||||||
|
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
|
||||||
|
|
||||||
|
const user = interaction.options.getUser('user');
|
||||||
|
const id = interaction.options.getString('id');
|
||||||
|
|
||||||
|
if (!user && !id) {
|
||||||
|
return interaction.reply({ content: 'No user or id given.', flags: [MessageFlags.Ephemeral] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const idToUse = (user?.id ?? id) as string;
|
||||||
|
|
||||||
|
const recordMessage = await getRecordMessageFromDiscordId(idToUse, 1, interaction.guild);
|
||||||
|
|
||||||
|
if (!recordMessage) return interaction.editReply('No records found on any linked accounts.');
|
||||||
|
|
||||||
|
interaction.editReply(recordMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
+15
-1
@@ -138,4 +138,18 @@ export type Ban = {
|
|||||||
export type BanUpdate = {
|
export type BanUpdate = {
|
||||||
action: 'create' | 'update' | 'delete'
|
action: 'create' | 'update' | 'delete'
|
||||||
ban: Ban
|
ban: Ban
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RecordCategory = 'positive' | 'negative' | 'neutral'
|
||||||
|
|
||||||
|
export type Record = {
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
creator_id: number
|
||||||
|
created_at: string
|
||||||
|
body: string
|
||||||
|
category: RecordCategory
|
||||||
|
updated_at: string
|
||||||
|
updater_id: number
|
||||||
|
is_deleted: boolean
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { APIRole, PermissionsBitField, TextChannel } from 'discord.js';
|
import { ActionRowBuilder, APIRole, ButtonBuilder, EmbedBuilder, PermissionsBitField, TextChannel } from 'discord.js';
|
||||||
|
|
||||||
|
export type MessageContent = { content?: string, embeds?: EmbedBuilder[], components: ActionRowBuilder<ButtonBuilder>[] };
|
||||||
|
|
||||||
export type RoleChangeLog = {
|
export type RoleChangeLog = {
|
||||||
key: '$add' | '$remove',
|
key: '$add' | '$remove',
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import { Database } from '../shared/Database';
|
|||||||
import { userIsBanned } from './e621-utils';
|
import { userIsBanned } from './e621-utils';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
|
|
||||||
|
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> {
|
export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise<string> {
|
||||||
const e621UserIds = await Database.getE621Ids(discordId);
|
const e621UserIds = await Database.getE621Ids(discordId);
|
||||||
|
|
||||||
@@ -42,4 +50,57 @@ export async function getDiscordAlts(e621Id: number, guild: Guild, depth = 1, ig
|
|||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function comprehensiveAltLookupFromDiscord(discordId: string, guild: Guild): Promise<AltData> {
|
||||||
|
return getE621AltData(discordId, guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function comprehensiveAltLookupFromE621(e621Id: number, guild: Guild): Promise<AltData> {
|
||||||
|
return getDiscordAltData(e621Id, guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getE621AltData(discordId: string, guild: Guild, 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 = !!(await guild.bans.fetch(discordId));
|
||||||
|
} 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, 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;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ import { Channel, GuildBasedChannel, GuildChannel, GuildTextBasedChannel, TextBa
|
|||||||
import { Database } from '../shared/Database';
|
import { Database } from '../shared/Database';
|
||||||
|
|
||||||
export async function channelIsInStaffCategory(channel: GuildBasedChannel) {
|
export async function channelIsInStaffCategory(channel: GuildBasedChannel) {
|
||||||
|
if (!channel.guildId) return false;
|
||||||
|
|
||||||
const staffCategories = await Database.getGuildStaffCategories(channel.guildId);
|
const staffCategories = await Database.getGuildStaffCategories(channel.guildId);
|
||||||
|
|
||||||
return staffCategories.includes(channel.parentId!);
|
return staffCategories.includes(channel.parentId!);
|
||||||
|
|||||||
+23
-71
@@ -1,18 +1,24 @@
|
|||||||
import { Guild } from 'discord.js';
|
import { Guild } from 'discord.js';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { E621Post, E621User } from '../types';
|
import { E621Post, E621User, Record } from '../types';
|
||||||
import { Database } from '../shared/Database';
|
import { Database } from '../shared/Database';
|
||||||
|
|
||||||
const BLACKLISTED_TAGS: string[] = [];
|
const BLACKLISTED_TAGS: string[] = [];
|
||||||
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
|
const BLACKLISTED_NONSAFE_TAGS: string[] = ['young'];
|
||||||
|
|
||||||
const USER_AGENT = 'E621DiscordBot';
|
const USER_AGENT = 'E621DiscordBot';
|
||||||
const E621_NAME_URL = `${config.E621_BASE_URL}/users/{idOrName}.json`;
|
|
||||||
const E621_POST_URL = `${config.E621_BASE_URL}/posts/{id}.json`;
|
|
||||||
const E621_MD5_POST_URL = `${config.E621_BASE_URL}/posts.json?md5={md5}`;
|
|
||||||
|
|
||||||
export async function getE621User(idOrName: string | number): Promise<E621User | null> {
|
async function request(path: string, query?: { [name: string]: string }): Promise<any> {
|
||||||
const res = await fetch(E621_NAME_URL.replace('{idOrName}', idOrName.toString()), {
|
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: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT
|
'User-Agent': USER_AGENT
|
||||||
}
|
}
|
||||||
@@ -20,31 +26,19 @@ export async function getE621User(idOrName: string | number): Promise<E621User |
|
|||||||
|
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
||||||
return await res.json() as E621User;
|
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> {
|
export async function getE621Post(id: string | number): Promise<E621Post | null> {
|
||||||
const res = await fetch(E621_POST_URL.replace('{id}', id.toString()), {
|
return (await request(`/posts/${id}`)).post as E621Post;
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return (await res.json() as { post: E621Post }).post as E621Post;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getE621PostByMd5(md5: string): Promise<E621Post | null> {
|
export async function getE621PostByMd5(md5: string): Promise<E621Post | null> {
|
||||||
const res = await fetch(E621_MD5_POST_URL.replace('{md5}', md5), {
|
return (await request('/posts', { md5 })).post as E621Post;
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
return (await res.json() as { post: E621Post }).post as E621Post;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasBlacklistedTags(post: E621Post): boolean {
|
export function hasBlacklistedTags(post: E621Post): boolean {
|
||||||
@@ -66,52 +60,10 @@ export async function userIsBanned(idOrName: string | number): Promise<boolean>
|
|||||||
return user?.is_banned ?? false;
|
return user?.is_banned ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AltData = {
|
export async function getUserRecords(id: number): Promise<Record[]> {
|
||||||
type: 'e621' | 'discord'
|
const records = await request('/user_feedbacks', { 'search[user_id]': id.toString() });
|
||||||
thisId: number | string
|
|
||||||
banned: boolean
|
|
||||||
alts: AltData[]
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function comprehensiveAltLookupFromDiscord(discordId: string, guild: Guild): Promise<AltData> {
|
if (records.user_feedbacks) return [];
|
||||||
return getE621Alts(discordId, guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function comprehensiveAltLookupFromE621(e621Id: number, guild: Guild): Promise<AltData> {
|
return records as Record[];
|
||||||
return getDiscordAlts(e621Id, guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getE621Alts(discordId: string, guild: Guild, 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 = !!(await guild.bans.fetch(discordId));
|
|
||||||
} 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 getDiscordAlts(e621Id, guild, depth + 1, toIgnore));
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDiscordAlts(e621Id: number, guild: Guild, 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 getE621Alts(discordId, guild, depth + 1, ignore));
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { ActionRowBuilder, APIEmbedField, ButtonBuilder, ButtonStyle, EmbedBuilder, GuildMember, MessageActionRowComponentBuilder, time } from 'discord.js';
|
import { ActionRowBuilder, APIEmbedField, ButtonBuilder, ButtonStyle, EmbedBuilder, GuildMember, MessageActionRowComponentBuilder, time } from 'discord.js';
|
||||||
import { Database } from '../shared/Database';
|
import { Database } from '../shared/Database';
|
||||||
import { Note } from '../types';
|
import { MessageContent, Note } from '../types';
|
||||||
|
|
||||||
type MessageContent = { content: string, components: ActionRowBuilder<ButtonBuilder>[] };
|
|
||||||
|
|
||||||
const NOTES_PER_PAGE = 5;
|
const NOTES_PER_PAGE = 5;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Embed, EmbedBuilder, Guild } from 'discord.js';
|
||||||
|
import { Database } from '../shared/Database';
|
||||||
|
import { E621User, MessageContent, Record, RecordCategory } from '../types';
|
||||||
|
import { getE621User, getUserRecords } from './e621-utils';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { e621IdsFromAltData, comprehensiveAltLookupFromDiscord } from './alt-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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user