From 0d43dd6abeaeba31298f31471d244b7c2ddd17fd Mon Sep 17 00:00:00 2001 From: friedger Date: Mon, 31 Mar 2025 12:38:30 +0200 Subject: [PATCH 1/2] feat: add revoke-role (WIP) --- src/commands/revoke-role.ts | 138 ++++++++++++++++++++++++++++++++++++ src/index.ts | 3 + src/register-commands.ts | 20 ++++++ 3 files changed, 161 insertions(+) create mode 100644 src/commands/revoke-role.ts diff --git a/src/commands/revoke-role.ts b/src/commands/revoke-role.ts new file mode 100644 index 0000000..4ce785a --- /dev/null +++ b/src/commands/revoke-role.ts @@ -0,0 +1,138 @@ +import { + BundlerService, + getAccountAddress, + getAccountBalance, + getCardAddress, +} from "@citizenwallet/sdk"; +import { ChatInputCommandInteraction, Client } from "discord.js"; +import { ethers, keccak256, toUtf8Bytes, Wallet } from "ethers"; +import { getCommunity } from "../cw"; +import { createDiscordMention } from "../utils/address"; +import { ContentResponse, generateContent } from "../utils/content"; +import { createProgressSteps } from "../utils/progress"; +import { getAddressFromUserInputWithReplies } from "./conversion/address"; + +export const handleRevokeRoleCommand = async ( + client: Client, + interaction: ChatInputCommandInteraction +) => { + await interaction.reply({ + content: createProgressSteps(0), + ephemeral: true, + }); + + const alias = interaction.options.getString("token"); + if (!alias) { + await interaction.editReply("You need to specify a token!"); + return; + } + + const role = interaction.options.getString("role"); + if (!role) { + await interaction.editReply("You need to specify a role!"); + return; + } + + const message = interaction.options.getString("message"); + + const community = getCommunity(alias); + const token = community.primaryToken; + + const guild = await client.guilds.fetch({ guild: interaction.guildId }); + const users = await guild.members.fetch(); + + const privateKey = process.env.BOT_PRIVATE_KEY; + if (!privateKey) { + await interaction.editReply({ + content: "Private key is not set", + }); + return; + } + + const signer = new Wallet(privateKey); + + const signerAccountAddress = await getAccountAddress( + community, + signer.address + ); + if (!signerAccountAddress) { + await interaction.editReply({ + content: "Could not find an account for you!", + }); + return; + } + + // signer setup done + await interaction.editReply(createProgressSteps(1)); + + const content: ContentResponse = { + header: "", + content: [], + }; + + for (const [userId, user] of users) { + const hashedUserId = keccak256(toUtf8Bytes(userId)); + + const cardAddress = await getCardAddress(community, hashedUserId); + if (!cardAddress) { + content.content.push("Could not find an account to send to!"); + await interaction.editReply({ + content: generateContent(content), + }); + continue; + } + + // check user status + const burnStatus = await getBurnStatus(user, role); + if (burnStatus.status === "burnt") { + content.content.push(`${user} has already burned`); + await interaction.editReply({ + content: generateContent(content), + }); + continue; + } else { + const balance = await getAccountBalance(community, cardAddress); + if (burnStatus.remainingBurns > balance) { + content.content.push(`${user} has not enough`); + await interaction.editReply({ + content: generateContent(content), + }); + await guild.members.removeRole({ + user: user, + role: role, + }); + } else { + const bundler = new BundlerService(community); + + try { + const hash = await bundler.burnFromERC20Token( + signer, + token.address, + signerAccountAddress, + cardAddress, + burnStatus.remainingBurns.toString(), + message + ); + } catch (e) {} + } + } + } + content.header = createProgressSteps(3); + + await interaction.editReply({ + content: generateContent(content), + }); + + content.header = "✅ Done"; + + await interaction.editReply({ + content: generateContent(content), + }); +}; + +const getBurnStatus = async (user: string, role: string) => { + return { + status: "burnt", + remainingBurns: 0, + }; +}; diff --git a/src/index.ts b/src/index.ts index d185339..cee89e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,9 @@ client.on(Events.InteractionCreate, async (interaction) => { case "burn-many": await handleBurnManyCommand(client, interaction); break; + case "revoke-role": + await handleRevokeRoleCommand(client, interaction); + break; case "add-owner": await handleAddOwnerCommand(client, interaction); break; diff --git a/src/register-commands.ts b/src/register-commands.ts index bff2ae6..db6fdb4 100644 --- a/src/register-commands.ts +++ b/src/register-commands.ts @@ -195,6 +195,26 @@ const getCommands = () => }, ], }, + { + name: "revoke-role", + description: "Revoke role if in debts!", + default_member_permissions: "32", + options: [ + { + name: "token", + description: "The token of debts", + type: 3, // STRING type + required: true, + autocomplete: true, + }, + { + name: "role", + description: "The role to revoke if user has not burnt enough tokens", + type: 3, // STRING type + required: true, + }, + ], + }, { name: "add-owner", description: "Add an owner to your Safe! 🔑", From 987e05d88e3033455bef2e8c16345bdb834d08d0 Mon Sep 17 00:00:00 2001 From: friedger Date: Wed, 2 Apr 2025 11:41:43 +0200 Subject: [PATCH 2/2] chore: rename revoke command to burn-or-revoke-role --- README.md | 12 ++ ...{revoke-role.ts => burn-or-revoke-role.ts} | 109 +++++++++++++----- src/index.ts | 5 +- src/openai/index.ts | 2 +- src/register-commands.ts | 12 +- 5 files changed, 106 insertions(+), 34 deletions(-) rename src/commands/{revoke-role.ts => burn-or-revoke-role.ts} (54%) diff --git a/README.md b/README.md index 7c52023..ba0d47d 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,15 @@ A Discord bot powered by discord.js and TypeScript. ``` For development with auto-restart: + +```bash +npm dev +``` + +## Local development + +Run cloudflared in a terminal and add he tunnel url to Url mapping in discord developer console + +```bash +docker run cloudflare/cloudflared:latest tunnel --url http://localhost:3000 +``` diff --git a/src/commands/revoke-role.ts b/src/commands/burn-or-revoke-role.ts similarity index 54% rename from src/commands/revoke-role.ts rename to src/commands/burn-or-revoke-role.ts index 4ce785a..610d04e 100644 --- a/src/commands/revoke-role.ts +++ b/src/commands/burn-or-revoke-role.ts @@ -4,15 +4,19 @@ import { getAccountBalance, getCardAddress, } from "@citizenwallet/sdk"; -import { ChatInputCommandInteraction, Client } from "discord.js"; -import { ethers, keccak256, toUtf8Bytes, Wallet } from "ethers"; +import { + APIRole, + ChatInputCommandInteraction, + Client, + GuildMember, + Role, +} from "discord.js"; +import { keccak256, toUtf8Bytes, Wallet } from "ethers"; import { getCommunity } from "../cw"; -import { createDiscordMention } from "../utils/address"; import { ContentResponse, generateContent } from "../utils/content"; import { createProgressSteps } from "../utils/progress"; -import { getAddressFromUserInputWithReplies } from "./conversion/address"; -export const handleRevokeRoleCommand = async ( +export const handleBurnOrRevokeRoleCommand = async ( client: Client, interaction: ChatInputCommandInteraction ) => { @@ -27,7 +31,13 @@ export const handleRevokeRoleCommand = async ( return; } - const role = interaction.options.getString("role"); + const amount = interaction.options.getNumber("amount"); + if (!amount) { + await interaction.editReply("You need to specify an amount!"); + return; + } + + const role = interaction.options.getRole("role"); if (!role) { await interaction.editReply("You need to specify a role!"); return; @@ -37,9 +47,7 @@ export const handleRevokeRoleCommand = async ( const community = getCommunity(alias); const token = community.primaryToken; - - const guild = await client.guilds.fetch({ guild: interaction.guildId }); - const users = await guild.members.fetch(); + const guild = await client.guilds.fetch(interaction.guildId); const privateKey = process.env.BOT_PRIVATE_KEY; if (!privateKey) { @@ -63,43 +71,67 @@ export const handleRevokeRoleCommand = async ( } // signer setup done - await interaction.editReply(createProgressSteps(1)); const content: ContentResponse = { - header: "", + header: createProgressSteps(1), content: [], }; + await interaction.editReply({ + content: generateContent(content), + }); + + const users = guild.members.cache.filter((member) => { + // check whether member is not a bot and has the given role + return !member.user.bot && member.roles.cache.has(role.id); + }); for (const [userId, user] of users) { const hashedUserId = keccak256(toUtf8Bytes(userId)); const cardAddress = await getCardAddress(community, hashedUserId); if (!cardAddress) { - content.content.push("Could not find an account to send to!"); + content.content.push( + `Could not find an account to send to for user ${user.user.displayName}!` + ); await interaction.editReply({ content: generateContent(content), }); continue; } + content.header = createProgressSteps(2); + await interaction.editReply({ + content: generateContent(content), + }); + // check user status - const burnStatus = await getBurnStatus(user, role); + const burnStatus = { status: "new", remainingBurns: amount }; //await getBurnStatus(user, role); + if (burnStatus.status === "burnt") { content.content.push(`${user} has already burned`); await interaction.editReply({ content: generateContent(content), }); - continue; } else { const balance = await getAccountBalance(community, cardAddress); + + content.content.push( + `Handling user: ${user}, balance: ${balance} ${token.symbol}` + ); + await interaction.editReply({ + content: generateContent(content), + }); + if (burnStatus.remainingBurns > balance) { - content.content.push(`${user} has not enough`); - await interaction.editReply({ - content: generateContent(content), - }); await guild.members.removeRole({ user: user, - role: role, + role: role.id, + }); + content.content.push( + `${user} has not enough ${token.symbol}, removed role ${role.name}.` + ); + await interaction.editReply({ + content: generateContent(content), }); } else { const bundler = new BundlerService(community); @@ -113,15 +145,33 @@ export const handleRevokeRoleCommand = async ( burnStatus.remainingBurns.toString(), message ); - } catch (e) {} + content.content.push( + `Burnt ${burnStatus.remainingBurns.toString()} ${ + token.symbol + } for ${user}: ${hash}` + ); + await interaction.editReply({ + content: generateContent(content), + }); + } catch (e) { + content.content.push( + `Failed to burnt ${burnStatus.remainingBurns.toString()} ${ + token.symbol + } for ${user} (${e.message})` + ); + await interaction.editReply({ + content: generateContent(content), + }); + } } } - } - content.header = createProgressSteps(3); - await interaction.editReply({ - content: generateContent(content), - }); + content.header = createProgressSteps(3); + + await interaction.editReply({ + content: generateContent(content), + }); + } content.header = "✅ Done"; @@ -130,9 +180,12 @@ export const handleRevokeRoleCommand = async ( }); }; -const getBurnStatus = async (user: string, role: string) => { +const getBurnStatus = async ( + user: GuildMember, + role: NonNullable +) => { return { - status: "burnt", - remainingBurns: 0, + status: "partial", + remainingBurns: 2, }; }; diff --git a/src/index.ts b/src/index.ts index cee89e8..0835192 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { handleShareAddressCommand } from "./commands/shareAddress"; import { handleBurnManyCommand } from "./commands/burn-many"; import { handleDoCommand } from "./commands/do"; import { startLiveUpdates } from "./live"; +import { handleBurnOrRevokeRoleCommand } from "./commands/burn-or-revoke-role"; // Create a new client instance const client = new Client({ @@ -109,8 +110,8 @@ client.on(Events.InteractionCreate, async (interaction) => { case "burn-many": await handleBurnManyCommand(client, interaction); break; - case "revoke-role": - await handleRevokeRoleCommand(client, interaction); + case "burn-or-revoke-role": + await handleBurnOrRevokeRoleCommand(client, interaction); break; case "add-owner": await handleAddOwnerCommand(client, interaction); diff --git a/src/openai/index.ts b/src/openai/index.ts index 134f961..8f6b26a 100644 --- a/src/openai/index.ts +++ b/src/openai/index.ts @@ -16,7 +16,7 @@ import { } from "../commands/do/tasks"; const client = new OpenAI({ - apiKey: process.env["OPENAI_API_KEY"], + apiKey: process.env["OPENAI_API_KEY"] || "invalid key", }); const constructSystemPrompt = ( diff --git a/src/register-commands.ts b/src/register-commands.ts index db6fdb4..975e421 100644 --- a/src/register-commands.ts +++ b/src/register-commands.ts @@ -196,8 +196,8 @@ const getCommands = () => ], }, { - name: "revoke-role", - description: "Revoke role if in debts!", + name: "burn-or-revoke-role", + description: "Burn tokens or revoke role if not enough!", default_member_permissions: "32", options: [ { @@ -207,10 +207,16 @@ const getCommands = () => required: true, autocomplete: true, }, + { + name: "amount", + description: "The amount to burn", + type: 10, // NUMBER type + required: true, + }, { name: "role", description: "The role to revoke if user has not burnt enough tokens", - type: 3, // STRING type + type: 8, // ROLE type required: true, }, ],