From 6455e5d6bc909f81651db023e633d0efb4584f05 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 3 Jul 2022 12:17:20 +0300 Subject: [PATCH] Discord bot plus roles command --- app/db/models/users.server.ts | 10 ++--- app/routes/plus/list.ts | 9 +++-- discord-bot/commands/access.ts | 44 ++++------------------ discord-bot/commands/index.ts | 8 +++- discord-bot/commands/plus.ts | 67 ++++++++++++++++++++++++++++++++++ discord-bot/ids.ts | 5 +++ discord-bot/index.ts | 4 +- discord-bot/package.json | 2 +- discord-bot/utils.ts | 40 ++++++++++++++++++++ 9 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 discord-bot/commands/plus.ts create mode 100644 discord-bot/utils.ts diff --git a/app/db/models/users.server.ts b/app/db/models/users.server.ts index 352c2d2ed..5e31fed99 100644 --- a/app/db/models/users.server.ts +++ b/app/db/models/users.server.ts @@ -91,11 +91,9 @@ SELECT "User"."discordId", "PlusTier"."tier" as "plusTier" WHERE "PlusTier"."tier" IS NOT NULL `); -export type FindAllPlusMembers = Pick< - UserWithPlusTier, - "discordId" | "plusTier" ->[]; - export function findAllPlusMembers() { - return findAllPlusMembersStm.all() as FindAllPlusMembers; + return findAllPlusMembersStm.all() as Array<{ + discordId: User["discordId"]; + plusTier: NonNullable; + }>; } diff --git a/app/routes/plus/list.ts b/app/routes/plus/list.ts index 82d5bd46d..83955f186 100644 --- a/app/routes/plus/list.ts +++ b/app/routes/plus/list.ts @@ -2,10 +2,9 @@ import { json } from "@remix-run/node"; import type { LoaderFunction } from "@remix-run/node"; import { db } from "~/db"; import { canAccessLohiEndpoint } from "~/permissions"; -import type { FindAllPlusMembers } from "~/db/models/users.server"; export interface PlusListLoaderData { - users: FindAllPlusMembers; + users: Record; } export const loader: LoaderFunction = ({ request }) => { @@ -13,5 +12,9 @@ export const loader: LoaderFunction = ({ request }) => { throw new Response(null, { status: 403 }); } - return json({ users: db.users.findAllPlusMembers() }); + return json({ + users: Object.fromEntries( + db.users.findAllPlusMembers().map((u) => [u.discordId, u.plusTier]) + ), + }); }; diff --git a/discord-bot/commands/access.ts b/discord-bot/commands/access.ts index a448b4ccd..e7a47cd72 100644 --- a/discord-bot/commands/access.ts +++ b/discord-bot/commands/access.ts @@ -1,20 +1,11 @@ import { SlashCommandBuilder } from "@discordjs/builders"; import { GuildMemberRoleManager } from "discord.js"; -import invariant from "tiny-invariant"; -import { LOHI_TOKEN_HEADER_NAME } from "~/constants"; -import type { PlusListLoaderData } from "~/routes/plus/list"; import ids from "../ids"; import type { BotCommand } from "../types"; +import { isPlusTierRoleId, plusTierToRoleId, usersWithAccess } from "../utils"; const COMMAND_NAME = "access"; -const plusTierRoles = [ - "", - ids.roles.plusOne, - ids.roles.plusTwo, - ids.roles.plusThree, -] as const; - export const accessCommand: BotCommand = { guilds: [ids.guilds.plusServer], name: COMMAND_NAME, @@ -25,18 +16,18 @@ export const accessCommand: BotCommand = { ), execute: async ({ interaction }) => { const { users } = await usersWithAccess(); - const ownUser = users.find((u) => u.discordId === interaction.user.id); + const usersPlusTier = users[interaction.user.id]; - if (!ownUser) { + if (!usersPlusTier) { return interaction.reply({ content: "You currently don't have access", ephemeral: true, }); } - const targetRoleId = plusTierRoles[ownUser.plusTier]; + const targetRoleId = plusTierToRoleId(usersPlusTier); if (!targetRoleId) { - throw new Error(`No role for plus tier "${ownUser.plusTier}"`); + throw new Error(`No role for plus tier ${usersPlusTier}`); } const roleManager = interaction.member?.roles as GuildMemberRoleManager; @@ -45,14 +36,12 @@ export const accessCommand: BotCommand = { if (alreadyHasRole) { return interaction.reply({ - content: `You have access to +${ownUser.plusTier} and already have the role for it`, + content: `You have access to +${usersPlusTier} and already have the role for it`, ephemeral: true, }); } - const roleIdsToDelete = usersRoleIds.filter((id) => - plusTierRoles.includes(id as any) - ); + const roleIdsToDelete = usersRoleIds.filter(isPlusTierRoleId); for (const roleIdToDelete of roleIdsToDelete) { await roleManager.remove(roleIdToDelete); @@ -61,25 +50,8 @@ export const accessCommand: BotCommand = { await roleManager.add(targetRoleId); return interaction.reply({ - content: `Gave the role giving access to +${ownUser.plusTier}`, + content: `Gave the role giving access to +${usersPlusTier}`, ephemeral: true, }); }, }; - -async function usersWithAccess(): Promise { - invariant(process.env["SENDOU_INK_URL"], "SENDOU_INK_URL is not set"); - invariant(process.env["LOHI_TOKEN"], "LOHI_TOKEN is not set"); - - const response = await fetch(`${process.env["SENDOU_INK_URL"]}/plus/list`, { - headers: [[LOHI_TOKEN_HEADER_NAME, process.env["LOHI_TOKEN"]]], - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch users. Response status was ${response.status}` - ); - } - - return response.json(); -} diff --git a/discord-bot/commands/index.ts b/discord-bot/commands/index.ts index aedec6e1c..47a13777a 100644 --- a/discord-bot/commands/index.ts +++ b/discord-bot/commands/index.ts @@ -1,9 +1,15 @@ import { questionCommand } from "./q"; import { lfgRoleCommand } from "./lfg"; import { accessCommand } from "./access"; +import { plusCommand } from "./plus"; import type { BotCommand } from "discord-bot/types"; -export const commands = [questionCommand, lfgRoleCommand, accessCommand]; +export const commands = [ + questionCommand, + lfgRoleCommand, + accessCommand, + plusCommand, +]; export const commandsMap = Object.fromEntries(commands.map((c) => [c.name, c])); diff --git a/discord-bot/commands/plus.ts b/discord-bot/commands/plus.ts new file mode 100644 index 000000000..a10384cb7 --- /dev/null +++ b/discord-bot/commands/plus.ts @@ -0,0 +1,67 @@ +import { SlashCommandBuilder } from "@discordjs/builders"; +import invariant from "tiny-invariant"; +import ids from "../ids"; +import type { BotCommand } from "../types"; +import { isPlusTierRoleId, plusTierToRoleId, usersWithAccess } from "../utils"; + +const COMMAND_NAME = "plus"; +const ACTION_ARG = "dry"; + +export const plusCommand: BotCommand = { + guilds: [ids.guilds.adminServer], + name: COMMAND_NAME, + builder: new SlashCommandBuilder() + .setName(COMMAND_NAME) + .setDescription("Remove roles from Plus Server from people who lost access") + .addBooleanOption((option) => + option + .setName(ACTION_ARG) + .setDescription("If true doesn't remove or give roles") + .setRequired(true) + ), + // @ts-expect-error TODO: fix. Library doesn't seem to extract API Message type so I could fix this error? + execute: async ({ interaction, client }) => { + const isDryRun = interaction.options.getBoolean(ACTION_ARG); + + await interaction.deferReply({ ephemeral: true }); + + const { users } = await usersWithAccess(); + + const plusServer = client.guilds.cache.find( + (g) => g.id === ids.guilds.plusServer + ); + invariant(plusServer); + + let removed = 0; + let added = 0; + + for (const [, member] of await plusServer.members.fetch()) { + const usersPlusMemberRoleIds = member.roles.cache + .filter((role) => isPlusTierRoleId(role.id)) + .map((r) => r.id); + + const userMemberStatus = users[member.id]; + const roleToGive = plusTierToRoleId(userMemberStatus); + + const rolesToRemove = usersPlusMemberRoleIds.filter( + (id) => id !== roleToGive + ); + + for (const idToRemove of rolesToRemove) { + if (!isDryRun) await member.roles.remove(idToRemove); + removed++; + } + + if (roleToGive && !usersPlusMemberRoleIds.includes(roleToGive)) { + if (!isDryRun) await member.roles.add(roleToGive); + added++; + } + } + + return interaction.editReply( + `Removed ${removed} roles, gave ${added}. Members in server: ${ + plusServer!.members.cache.size + }` + ); + }, +}; diff --git a/discord-bot/ids.ts b/discord-bot/ids.ts index 1bf810b57..2a94c3c99 100644 --- a/discord-bot/ids.ts +++ b/discord-bot/ids.ts @@ -9,6 +9,10 @@ const ids = { plusOne: "657613334249734165", plusTwo: "678169134416265260", plusThree: "815267885601521696", + // TESTING VALUES + // plusOne: "992852036334911638", + // plusTwo: "992852055251234936", + // plusThree: "992852066403893258", patreonSupporter: "787281384909045760", patreonSupporterPlus: "787281624152801350", patreonSupporterPlusPlus: "787281663138725928", @@ -22,6 +26,7 @@ const ids = { sro: "737359708276654121", plusServer: "351828612208525333", sendou: "299182152161951744", + adminServer: "986325847323582474", }, } as const; diff --git a/discord-bot/index.ts b/discord-bot/index.ts index 0da12564b..c7b9bf5cb 100644 --- a/discord-bot/index.ts +++ b/discord-bot/index.ts @@ -6,7 +6,9 @@ import { commandsMap } from "./commands"; invariant(process.env["BOT_TOKEN"], "DISCORD_TOKEN must be set"); -const client = new Client({ intents: [Intents.FLAGS.GUILDS] }); +const client = new Client({ + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MEMBERS], +}); client.once("ready", () => { // eslint-disable-next-line no-console diff --git a/discord-bot/package.json b/discord-bot/package.json index 6b4a269ef..f66dc4404 100644 --- a/discord-bot/package.json +++ b/discord-bot/package.json @@ -2,7 +2,7 @@ "name": "lohi", "scripts": { "dev": "node --experimental-fetch --require ts-node/register --require tsconfig-paths/register index.ts", - "commands": "ts-node deploy-commands.ts", + "commands": "node --experimental-fetch --require ts-node/register --require tsconfig-paths/register deploy-commands.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit" }, diff --git a/discord-bot/utils.ts b/discord-bot/utils.ts new file mode 100644 index 000000000..932563671 --- /dev/null +++ b/discord-bot/utils.ts @@ -0,0 +1,40 @@ +import invariant from "tiny-invariant"; +import { LOHI_TOKEN_HEADER_NAME } from "~/constants"; +import type { PlusListLoaderData } from "~/routes/plus/list"; +import ids from "./ids"; + +export async function usersWithAccess(): Promise { + invariant(process.env["SENDOU_INK_URL"], "SENDOU_INK_URL is not set"); + invariant(process.env["LOHI_TOKEN"], "LOHI_TOKEN is not set"); + + const response = await fetch(`${process.env["SENDOU_INK_URL"]}/plus/list`, { + headers: [[LOHI_TOKEN_HEADER_NAME, process.env["LOHI_TOKEN"]]], + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch users. Response status was ${response.status}` + ); + } + + return response.json(); +} + +const memberRoles = [ + "", + ids.roles.plusOne, + ids.roles.plusTwo, + ids.roles.plusThree, +] as const; +export function plusTierToRoleId(tier?: number) { + if (!tier) return; + + const result = memberRoles[tier ?? 0]; + if (!result) return; + + return result; +} + +export function isPlusTierRoleId(id: string) { + return memberRoles.slice(1).includes(id as any); +}