mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Discord bot plus roles command
This commit is contained in:
@@ -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"]>;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
|
||||
67
discord-bot/commands/plus.ts
Normal file
67
discord-bot/commands/plus.ts
Normal 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
|
||||
}`
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
40
discord-bot/utils.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user