From a89cb6d81311543e67c94187d91aab67bdf260c4 Mon Sep 17 00:00:00 2001 From: SKINMAKER Date: Sun, 20 Oct 2024 00:28:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B4=87=20/=20=EC=84=9C=EB=B2=84=20-?= =?UTF-8?q?=20=EC=86=8C=EC=9C=A0=EC=A3=BC=20=EA=B0=84=20=EA=B4=80=EA=B3=84?= =?UTF-8?q?=EB=8F=84=20=EA=B0=9C=EC=84=A0=20(#659)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement owners mapping table * feat: add manual deploy * fix: query by vanity does not work * fix: do not fetch users on list queries * fix: updating owners does not work properly * fix: owner is not shown properly * ci: remove branch input on manual publish --------- Co-authored-by: Eunwoo Choi --- .eslintrc.js | 1 + pages/pendingBots/[id]/[date].tsx | 17 +- tsconfig.json | 2 +- types/index.ts | 7 +- utils/Query.ts | 297 ++++++++++++++++-------------- utils/Tools.ts | 6 + 6 files changed, 175 insertions(+), 155 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6c59e1c..ced41ba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-vars': ['warn'], + '@typescript-eslint/no-explicit-any': 'off', quotes: ['error', 'single'], semi: ['error', 'never'], }, diff --git a/pages/pendingBots/[id]/[date].tsx b/pages/pendingBots/[id]/[date].tsx index 72cc269..3ba2b5f 100644 --- a/pages/pendingBots/[id]/[date].tsx +++ b/pages/pendingBots/[id]/[date].tsx @@ -7,7 +7,7 @@ import { get } from '@utils/Query' import { BotSubmissionDenyReasonPresetsName, git } from '@utils/Constants' import Day from '@utils/Day' -import { SubmittedBot, User } from '@types' +import { SubmittedBot } from '@types' import useClipboard from 'react-use-clipboard' import { ParsedUrlQuery } from 'querystring' @@ -146,15 +146,12 @@ const PendingBot: NextPage = ({ data }) => { ))}

제작자

- {(data.owners as User[]).map((el) => ( - - ))} +
{data.discord && ( { export interface SubmittedBot { id: string date: number - owners: User[] + owner: User lib: Library prefix: string intro: string @@ -378,3 +378,8 @@ export interface ResponseProps { interface Data { [key: string]: T } + +export enum ObjectType { + Bot = 1, + Server = 2, +} diff --git a/utils/Query.ts b/utils/Query.ts index 4001629..a23c388 100644 --- a/utils/Query.ts +++ b/utils/Query.ts @@ -21,6 +21,7 @@ import { Webhook, BotSpec, ServerSpec, + ObjectType, } from '@types' import { botCategories, @@ -35,143 +36,148 @@ import knex from './Knex' import { Bots, Servers } from './Mongo' import { DiscordBot, getMainGuild } from './DiscordBot' import { sign, verify } from './Jwt' -import { camoUrl, formData, getYYMMDD, serialize } from './Tools' +import { areArraysEqual, camoUrl, formData, getYYMMDD, serialize } from './Tools' import { AddBotSubmit, AddServerSubmit, ManageBot, ManageServer } from './Yup' import { markdownImage } from './Regex' export const imageRateLimit = new TLRU({ maxAgeMs: 60000 }) async function getBot(id: string, topLevel = true): Promise { - const res = await knex('bots') + const [res] = (await knex('bots') .select([ - 'id', - 'flags', - 'owners', - 'lib', - 'prefix', - 'votes', - 'servers', - 'shards', - 'intro', - 'desc', - 'web', - 'git', - 'url', - 'category', - 'status', - 'trusted', - 'partnered', - 'discord', - 'state', - 'vanity', - 'bg', - 'banner', + 'bots.id', + 'bots.flags', + 'bots.lib', + 'bots.prefix', + 'bots.votes', + 'bots.servers', + 'bots.shards', + 'bots.intro', + 'bots.desc', + 'bots.web', + 'bots.git', + 'bots.url', + 'bots.category', + 'bots.status', + 'bots.trusted', + 'bots.partnered', + 'bots.discord', + 'bots.state', + 'bots.vanity', + 'bots.bg', + 'bots.banner', + knex.raw('JSON_ARRAYAGG(owners_mapping.user_id) as owners'), ]) - .where({ id }) + .leftJoin('owners_mapping', 'bots.id', 'owners_mapping.target_id') + .where({ 'bots.id': id }) .orWhere({ vanity: id, trusted: true }) .orWhere({ vanity: id, partnered: true }) - if (res[0]) { - const discordBot = await get.discord.user.load(res[0].id) + .groupBy('bots.id')) as any[] + + if (res) { + const discordBot = await get.discord.user.load(res.id) if (!discordBot) return null const botMember = (await getMainGuild() - ?.members?.fetch(res[0].id) + ?.members?.fetch(res.id) .catch((e) => e)) as GuildMember const name = discordBot.displayName - res[0].flags = - res[0].flags | + res.flags = + res.flags | (discordBot.flags.bitfield & DiscordUserFlags.VERIFIED_BOT ? BotFlags.verified : 0) | - (res[0].trusted ? BotFlags.trusted : 0) | - (res[0].partnered ? BotFlags.partnered : 0) - res[0].tag = discordBot.discriminator - res[0].avatar = discordBot.avatar - res[0].name = name - res[0].category = JSON.parse(res[0].category) - res[0].owners = JSON.parse(res[0].owners) + (res.trusted ? BotFlags.trusted : 0) | + (res.partnered ? BotFlags.partnered : 0) + res.tag = discordBot.discriminator + res.avatar = discordBot.avatar + res.name = name + res.category = JSON.parse(res.category) + res.owners = JSON.parse(res.owners) if (discordBot.flags.bitfield & UserFlags.BotHTTPInteractions) { - res[0].status = 'online' + res.status = 'online' } else if (botMember) { if (!botMember.presence) { - res[0].status = 'offline' + res.status = 'offline' } else { - res[0].status = botMember.presence.activities.some((r) => r.type === ActivityType.Streaming) + res.status = botMember.presence.activities.some((r) => r.type === ActivityType.Streaming) ? 'streaming' : botMember.presence.status } } else { - res[0].status = null + res.status = null } - delete res[0].trusted - delete res[0].partnered + delete res.trusted + delete res.partnered if (topLevel) { - res[0].owners = await Promise.all( - res[0].owners.map(async (u: string) => await get._rawUser.load(u)) + res.owners = await Promise.all( + res.owners.map(async (u: string) => await get._rawUser.load(u)) ) - res[0].owners = res[0].owners.filter((el: User | null) => el).map((row: User) => ({ ...row })) + res.owners = res.owners.filter((el: User | null) => el).map((row: User) => ({ ...row })) } await knex('bots').update({ name }).where({ id }) } - return res[0] ?? null + return res ?? null } async function getServer(id: string, topLevel = true): Promise { - const res = await knex('servers') - .select([ - 'id', - 'name', - 'flags', - 'intro', - 'desc', - 'votes', - 'owners', - 'category', - 'invite', - 'state', - 'vanity', - 'bg', - 'banner', - 'flags', - ]) - .where({ id }) - .orWhereRaw(`(flags & ${ServerFlags.trusted}) and vanity=?`, [id]) - .orWhereRaw(`(flags & ${ServerFlags.partnered}) and vanity=?`, [id]) - if (res[0]) { - const data = await getServerData(res[0].id) + const [res] = await knex('servers') + .select([ + 'servers.id', + 'servers.name', + 'servers.flags', + 'servers.intro', + 'servers.desc', + 'servers.votes', + knex.raw('JSON_ARRAYAGG(owners_mapping.user_id) as owners'), + 'servers.category', + 'servers.invite', + 'servers.state', + 'servers.vanity', + 'servers.bg', + 'servers.banner', + 'servers.flags', + ]) + .leftJoin('owners_mapping', 'servers.id', 'owners_mapping.target_id') + .where({ 'servers.id': id }) + .orWhereRaw(`(servers.flags & ${ServerFlags.trusted}) and servers.vanity=?`, [id]) + .orWhereRaw(`(servers.flags & ${ServerFlags.partnered}) and servers.vanity=?`, [id]) + .groupBy('servers.id') as any[] + + if (res) { + const data = await getServerData(res.id) + res.owners = JSON.parse(res.owners) if (!data || +new Date() - +new Date(data.updatedAt) > 3 * 60 * 1000) { - if (res[0].state === 'ok') res[0].state = 'unreachable' + if (res.state === 'ok') res.state = 'unreachable' } else { - res[0].flags = - res[0].flags | + res.flags = + res.flags | (data.features.includes(GuildFeature.Partnered) && ServerFlags.discord_partnered) | (data.features.includes(GuildFeature.Verified) && ServerFlags.verified) - if ( - res[0].owners !== JSON.stringify([data.owner, ...data.admins]) || - res[0].name !== data.name - ) - await knex('servers') - .update({ name: data.name, owners: JSON.stringify([data.owner, ...data.admins]) }) - .where({ id: res[0].id }) + if (res.name !== data.name) + await knex('servers').update({ name: data.name }).where({ id: res.id }) + if (!areArraysEqual(res.owners, [data.owner, ...data.admins])) { + updateOwners(res.id, [data.owner, ...data.admins], 'server') + } } - delete res[0].owners - res[0].icon = data?.icon || null - res[0].members = data?.memberCount || null - res[0].emojis = data?.emojis || [] - res[0].category = JSON.parse(res[0].category) - res[0].boostTier = data?.boostTier ?? null + delete res.owners + res.icon = data?.icon || null + res.members = data?.memberCount || null + res.emojis = data?.emojis || [] + res.category = JSON.parse(res.category) + res.boostTier = data?.boostTier ?? null if (topLevel) { - res[0].owner = (await get._rawUser.load(data?.owner || '')) || null - res[0].bots = + res.owner = (await get._rawUser.load(data?.owner || '')) || null + res.bots = (await Promise.all(data?.bots.slice(0, 3).map((el) => get._rawBot.load(el)) || [])).filter( (el) => el ) || null } else { - res[0].owner = data?.owner || null - res[0].bots = data?.bots || null + res.owner = data?.owner || null + res.bots = data?.bots || null } } - return res[0] ?? null + return res ?? null } async function fetchServerOwners(id: string): Promise { @@ -191,14 +197,12 @@ async function getServerData(id: string): Promise { async function getUser(id: string, topLevel = true): Promise { const res = await knex('users').select(['id', 'flags', 'github']).where({ id }) if (res[0]) { - const ownedBots = await knex('bots') - .select(['id']) - .where('owners', 'like', `%${id}%`) - .orderBy('date', 'asc') - const ownedServer = await knex('servers') - .select(['id']) - .where('owners', 'like', `%${id}%`) - .orderBy('date', 'asc') + const ownedList = await knex('owners_mapping') + .select(['target_id', 'type']) + .where({ user_id: id }) + + const ownedBots = ownedList.filter((i) => i.type === ObjectType.Bot) + const ownedServers = ownedList.filter((i) => i.type === ObjectType.Server) const discordUser = await get.discord.user.load(id) res[0].tag = discordUser?.discriminator || '0000' @@ -206,14 +210,14 @@ async function getUser(id: string, topLevel = true): Promise { res[0].globalName = discordUser?.displayName || 'Unknown User' if (topLevel) { res[0].bots = ( - await Promise.all(ownedBots.map(async (b) => await get._rawBot.load(b.id))) + await Promise.all(ownedBots.map(async (b) => await get._rawBot.load(b.target_id))) ).filter((el: Bot | null) => el) res[0].servers = ( - await Promise.all(ownedServer.map(async (b) => await get._rawServer.load(b.id))) + await Promise.all(ownedServers.map(async (b) => await get._rawServer.load(b.target_id))) ).filter((el: Server | null) => el) } else { - res[0].bots = ownedBots.map((el) => el.id) - res[0].servers = ownedServer.map((el) => el.id) + res[0].bots = ownedBots.map((el) => el.target_id) + res[0].servers = ownedServers.map((el) => el.target_id) } } return res[0] || null @@ -312,7 +316,7 @@ async function getBotList(type: ListType, page = 1, query?: string): Promise await getBot(el.id)))).map((r) => ({ ...r })), + data: (await Promise.all(res.map(async (el) => await getBot(el.id, false)))).map((r) => ({ ...r })), currentPage: page, totalPage: Math.ceil(Number(count) / (type === 'SEARCH' ? 8 : 16)), } @@ -417,7 +421,7 @@ async function getServerList(type: ListType, page = 1, query?: string): Promise< } return { type, - data: (await Promise.all(res.map(async (el) => await getServer(el.id)))).map((r) => ({ ...r })), + data: (await Promise.all(res.map(async (el) => await getServer(el.id, false)))).map((r) => ({ ...r })), currentPage: page, totalPage: Math.ceil(Number(count) / (type === 'SEARCH' ? 8 : 16)), } @@ -438,15 +442,13 @@ async function getBotSubmit(id: string, date: number): Promise { 'git', 'discord', 'state', - 'owners', + 'owner', 'reason', ]) .where({ id, date }) if (res.length === 0) return null res[0].category = JSON.parse(res[0].category) - res[0].owners = await Promise.all( - JSON.parse(res[0].owners).map(async (u: string) => await get.user.load(u)) - ) + res[0].owner = await get.user.load(res[0].owner) return res[0] } @@ -466,17 +468,16 @@ async function getBotSubmits(id: string): Promise { 'git', 'discord', 'state', - 'owners', + 'owner', 'reason', ]) .orderBy('date', 'desc') - .where('owners', 'LIKE', `%${id}%`) + .where({ owner: id }) + const owner = await get.user.load(id) res = await Promise.all( res.map(async (el) => { el.category = JSON.parse(el.category) - el.owners = await Promise.all( - JSON.parse(el.owners).map(async (u: string) => await get.user.load(u)) - ) + el.owner = owner return el }) ) @@ -569,10 +570,7 @@ async function submitBot( id: string, data: AddBotSubmit ): Promise<1 | 2 | 3 | 4 | 5 | SubmittedBot> { - const submits = await knex('submitted') - .select(['id']) - .where({ state: 0 }) - .andWhere('owners', 'LIKE', `%${id}%`) + const submits = await knex('submitted').select(['id']).where({ state: 0 }) if (submits.length > 1) return 1 const botId = data.id const strikes = await get.botSubmitStrikes(botId) @@ -591,7 +589,7 @@ async function submitBot( await knex('submitted').insert({ id: botId, date: date, - owners: JSON.stringify([id]), + owner: id, lib: data.library, prefix: data.prefix, intro: data.intro, @@ -629,22 +627,25 @@ async function submitServer( await knex('servers').insert({ id: id, name: serverData.name, - owners: JSON.stringify([serverData.owner, ...serverData.admins]), intro: data.intro, desc: data.desc, category: JSON.stringify(data.category), invite: data.invite, token: sign({ id }), }) + await updateOwners(id, [serverData.owner, ...serverData.admins], 'server') get.server.clear(id) return true } async function getBotSpec(id: string, userID: string): Promise { const res = await knex('bots') - .select(['id', 'token', 'webhook_url', 'webhook_status']) - .where({ id }) - .andWhere('owners', 'like', `%${userID}%`) + .select(['bots.id', 'bots.token', 'bots.webhook_url', 'bots.webhook_status']) + .leftJoin('owners_mapping', 'bots.id', 'owners_mapping.target_id') + .where('owners_mapping.user_id', userID) + .andWhere('owners_mapping.type', ObjectType.Bot) + .andWhere('bots.id', id) + if (!res[0]) return null return { id: res[0].id, @@ -656,9 +657,12 @@ async function getBotSpec(id: string, userID: string): Promise { async function getServerSpec(id: string, userID: string): Promise { const res = await knex('servers') - .select(['id', 'token', 'webhook_url', 'webhook_status']) - .where({ id }) - .andWhere('owners', 'like', `%${userID}%`) + .select(['servers.id', 'servers.token', 'servers.webhook_url', 'servers.webhook_status']) + .leftJoin('owners_mapping', 'servers.id', 'owners_mapping.target_id') + .where('owners_mapping.user_id', userID) + .andWhere('owners_mapping.type', ObjectType.Server) + .andWhere('servers.id', id) + if (!res[0]) return null return { id: res[0].id, @@ -754,10 +758,15 @@ async function updateWebhook(id: string, type: 'bots' | 'servers', value: Partia return true } -async function updateOwner(id: string, owners: string[]): Promise { - await knex('bots') - .where({ id }) - .update({ owners: JSON.stringify(owners) }) +async function updateOwners(id: string, owners: string[], type: 'bot' | 'server'): Promise { + await knex('owners_mapping').where({ target_id: id }).del() + await knex('owners_mapping').insert( + owners.map((el) => ({ + user_id: el, + target_id: id, + type: type === 'bot' ? ObjectType.Bot : ObjectType.Server, + })) + ) get.bot.clear(id) } @@ -957,7 +966,7 @@ async function getBotSubmitStrikes(id: string) { } async function approveBotSubmission(id: string, date: number) { - const data = await knex('submitted') + const [data] = await knex('submitted') .select([ 'id', 'date', @@ -971,27 +980,27 @@ async function approveBotSubmission(id: string, date: number) { 'git', 'discord', 'state', - 'owners', + 'owner', 'reason', ]) .where({ state: 0, id, date }) - if (!data[0]) return false + if (!data) return false await knex('submitted').where({ state: 0, id, date }).update({ state: 1 }) await knex('bots').insert({ id, date, - owners: data[0].owners, - lib: data[0].lib, - prefix: data[0].prefix, - intro: data[0].intro, - desc: data[0].desc, - url: data[0].url, - web: data[0].web, - git: data[0].git, - category: data[0].category, - discord: data[0].discord, + lib: data.lib, + prefix: data.prefix, + intro: data.intro, + desc: data.desc, + url: data.url, + web: data.web, + git: data.git, + category: data.category, + discord: data.discord, token: sign({ id }), }) + updateOwners(id, [data.owner], 'bot') return true } @@ -1246,7 +1255,9 @@ export const update = { Github, bot: updateBot, server: updatedServer, - botOwners: updateOwner, + botOwners: (id: string, owners: string[]) => { + return updateOwners(id, owners, 'bot') + }, webhook: updateWebhook, denyBotSubmission, approveBotSubmission, diff --git a/utils/Tools.ts b/utils/Tools.ts index 067e6ac..6904be1 100644 --- a/utils/Tools.ts +++ b/utils/Tools.ts @@ -248,4 +248,10 @@ export function convertMetrixToGraph(data: MetrixData[], keyname?: string) { })) } +export function areArraysEqual(a: T[], b: T[]): boolean { + return ( + new Set(a).size === new Set(b).size && [...new Set(a)].every((item) => new Set(b).has(item)) + ) +} + export * from './ShowdownExtensions'