Discord bot plus roles command

This commit is contained in:
Kalle
2022-07-03 12:17:20 +03:00
parent 13dce6ba17
commit 6455e5d6bc
9 changed files with 141 additions and 48 deletions

View File

@@ -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<UserWithPlusTier["plusTier"]>;
}>;
}

View File

@@ -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<string, number>;
}
export const loader: LoaderFunction = ({ request }) => {
@@ -13,5 +12,9 @@ export const loader: LoaderFunction = ({ request }) => {
throw new Response(null, { status: 403 });
}
return json<PlusListLoaderData>({ users: db.users.findAllPlusMembers() });
return json<PlusListLoaderData>({
users: Object.fromEntries(
db.users.findAllPlusMembers().map((u) => [u.discordId, u.plusTier])
),
});
};

View File

@@ -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<PlusListLoaderData> {
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();
}

View File

@@ -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]));

View File

@@ -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
}`
);
},
};

View File

@@ -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;

View File

@@ -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

View File

@@ -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"
},

40
discord-bot/utils.ts Normal file
View File

@@ -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<PlusListLoaderData> {
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);
}