From 2c156c68537ad6668f46c6c07ad8fbd509d36cbb Mon Sep 17 00:00:00 2001 From: Snazzah Date: Thu, 24 Jun 2021 23:50:10 -0500 Subject: [PATCH] Add admin commands --- src/commands/admin/abandonall.ts | 67 +++++++++++++++++++++++++ src/commands/admin/adminbalance.ts | 24 +++++++++ src/commands/admin/allsupports.ts | 71 ++++++++++++++++++++++++++ src/commands/admin/deleteaccount.ts | 48 ++++++++++++++++++ src/commands/admin/deleteall.ts | 47 ++++++++++++++++++ src/commands/admin/deposit.ts | 25 ++++++++++ src/commands/admin/fund.ts | 40 +++++++++++++++ src/commands/admin/fundall.ts | 77 +++++++++++++++++++++++++++++ src/commands/admin/listall.ts | 51 +++++++++++++++++++ src/commands/admin/sync.ts | 23 +++++++++ src/commands/admin/withdraw.ts | 53 ++++++++++++++++++++ 11 files changed, 526 insertions(+) create mode 100644 src/commands/admin/abandonall.ts create mode 100644 src/commands/admin/adminbalance.ts create mode 100644 src/commands/admin/allsupports.ts create mode 100644 src/commands/admin/deleteaccount.ts create mode 100644 src/commands/admin/deleteall.ts create mode 100644 src/commands/admin/deposit.ts create mode 100644 src/commands/admin/fund.ts create mode 100644 src/commands/admin/fundall.ts create mode 100644 src/commands/admin/listall.ts create mode 100644 src/commands/admin/sync.ts create mode 100644 src/commands/admin/withdraw.ts diff --git a/src/commands/admin/abandonall.ts b/src/commands/admin/abandonall.ts new file mode 100644 index 0000000..e498f93 --- /dev/null +++ b/src/commands/admin/abandonall.ts @@ -0,0 +1,67 @@ +import { oneLine } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { confirm, resolveUser } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class AbandonAllCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'abandonall', + description: 'Abandons all supports of all accounts or of a given account.', + category: 'Admin', + aliases: ['abanall', 'dropall'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['abandonall', 'abandonall @user'], + usage: '[user]' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + // Single user abandon + if (ctx.args[0]) { + const discordID = resolveUser(ctx.args[0]); + if (!discordID) return "That Discord user isn't valid."; + + const account = await this.lbryx.ensureAccount(discordID, false); + if (!account.id) return 'That user does not have an account.'; + + const supportsCount = await this.lbryx.getSupportsCount(account.id); + if (supportsCount <= 0) return 'That user does not have any supports.'; + + if ( + !(await confirm( + ctx, + oneLine` + Are you sure you want to abandon **all supports** from that account? + *(${supportsCount.toLocaleString()} support[s])* + ` + )) + ) + return; + await this.lbryx.abandonAllClaims(account.id); + return `Abandoned ${supportsCount.toLocaleString()} support(s).`; + } + + // Abandon ALL supports + + await this.lbryx.sync(); + const pairs = this.lbryx.getIDs(); + if (pairs.length <= 0) return 'No pairs in the database.'; + + if (!(await confirm(ctx, 'Are you sure you want to abandon **all supports** from **all accounts**?'))) + return; + + await this.client.startTyping(ctx.channel.id); + let count = 0; + for (const [, lbryID] of pairs) { + const result = await this.lbryx.abandonAllClaims(lbryID); + if (result) count += result.count; + } + this.client.stopTyping(ctx.channel.id); + return `Abandoned ${count.toLocaleString()} supports(s).`; + } +} diff --git a/src/commands/admin/adminbalance.ts b/src/commands/admin/adminbalance.ts new file mode 100644 index 0000000..9e24612 --- /dev/null +++ b/src/commands/admin/adminbalance.ts @@ -0,0 +1,24 @@ +import { DexareClient } from 'dexare'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class AdminBalanceCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'adminbalance', + description: 'Shows the master wallet balance.', + category: 'Admin', + aliases: ['abal', 'adminbal'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['adminbalance'] + } + }); + + this.filePath = __filename; + } + + async run() { + const wallet = await this.lbry.walletBalance(); + return this.displayWallet(wallet, 'Master Wallet Balance'); + } +} diff --git a/src/commands/admin/allsupports.ts b/src/commands/admin/allsupports.ts new file mode 100644 index 0000000..72baf93 --- /dev/null +++ b/src/commands/admin/allsupports.ts @@ -0,0 +1,71 @@ +import { CommandContext, DexareClient } from 'dexare'; +import { Support } from '../../modules/lbry/types'; +import { GeneralCommand } from '../../util/abstracts'; +import { paginate } from '../../util/pager'; + +export default class AllSupportsommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'allsupports', + description: 'List all supports from all users.', + category: 'Admin', + aliases: ['asups', 'allsups'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['allsupports', 'allsupports @channel#a/video#b'], + usage: '[claim]' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + let claim: string | null = null; + if (ctx.args[0]) { + claim = await this.lbryx.resolveClaim(ctx.args[0]); + if (!claim) return "That claim isn't valid."; + } + + await this.lbryx.sync(); + const pairs = this.lbryx.getIDs(); + if (pairs.length <= 0) return 'No users found in the database.'; + + const allSupports: (Support & { + discordID: string; + accountID: string; + })[] = []; + + for (const [discordID, accountID] of pairs) { + const supportsCount = await this.lbryx.getSupportsCount(accountID); + if (supportsCount <= 0) continue; + const supports = await this.lbry.supportList({ + account_id: accountID, + page_size: supportsCount, + claim_id: claim || undefined + }); + for (const support of supports.items) + allSupports.push({ + ...support, + discordID, + accountID + }); + } + + if (allSupports.length <= 0) return 'No supports found.'; + + await paginate( + ctx, + { + title: 'Supports', + items: allSupports.map( + (item) => `> ${item.name} \`${item.claim_id}\`\n> <@${item.discordID}> ${item.amount} LBC` + ), + itemSeparator: '\n\n' + }, + { + author: { name: `All supports ${claim ? ` on claim \`${claim}\`` : ''}` } + } + ); + } +} diff --git a/src/commands/admin/deleteaccount.ts b/src/commands/admin/deleteaccount.ts new file mode 100644 index 0000000..0fcc454 --- /dev/null +++ b/src/commands/admin/deleteaccount.ts @@ -0,0 +1,48 @@ +import { stripIndents } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { confirm, resolveUser } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class DeleteAccountommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'deleteaccount', + description: "Delete a user's Curation account.", + category: 'Admin', + aliases: ['del', 'delacc'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['deleteaccount @user'], + usage: '' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + const discordID = resolveUser(ctx.args[0]); + if (!discordID) return "That Discord user isn't valid."; + const account = await this.lbryx.ensureAccount(discordID, false); + if (!account.id) return 'That user does not have an account.'; + + const supportsCount = await this.lbryx.getSupportsCount(account.id); + if ( + !(await confirm( + ctx, + `Are you sure you want to delete that account? *(${supportsCount.toLocaleString()} support[s])*` + )) + ) + return; + + try { + await this.lbryx.deleteAccount(discordID, account.id); + return 'Deleted account.'; + } catch (e) { + return stripIndents` + Failed to delete the account. An error most likely occured while backing up the wallet. + \`\`\`\n${e.toString()}\`\`\` + `; + } + } +} diff --git a/src/commands/admin/deleteall.ts b/src/commands/admin/deleteall.ts new file mode 100644 index 0000000..8f431bf --- /dev/null +++ b/src/commands/admin/deleteall.ts @@ -0,0 +1,47 @@ +import { stripIndents } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { confirm } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class DeleteAllCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'deleteall', + description: 'Deletes all accounts in the database.', + category: 'Admin', + aliases: ['delall'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['delall'] + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + await this.lbryx.sync(); + const pairs = this.lbryx.getIDs(); + + if (pairs.length <= 0) return 'No pairs in the database.'; + if ( + !(await confirm( + ctx, + `Are you sure you want to delete **all** ${pairs.length.toLocaleString()} accounts?` + )) + ) + return; + + for (const [discordID, lbryID] of pairs) { + try { + await this.lbryx.deleteAccount(discordID, lbryID); + } catch (e) { + return stripIndents` + Failed to delete an account. An error most likely occured while backing up the wallet. + \`\`\`\n${e.toString()}\`\`\` + `; + } + } + return 'Deleted all accounts.'; + } +} diff --git a/src/commands/admin/deposit.ts b/src/commands/admin/deposit.ts new file mode 100644 index 0000000..e74dac4 --- /dev/null +++ b/src/commands/admin/deposit.ts @@ -0,0 +1,25 @@ +import { DexareClient } from 'dexare'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class DepositCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'deposit', + description: 'Gets the address of the master wallet.', + category: 'Admin', + aliases: ['dp'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['deposit'] + } + }); + + this.filePath = __filename; + } + + async run() { + const accountID = await this.lbryx.getDefaultAccount(); + const addresses = await this.lbry.addressList({ account_id: accountID, page_size: 1 }); + return `Address: ${addresses.items[0].address}`; + } +} diff --git a/src/commands/admin/fund.ts b/src/commands/admin/fund.ts new file mode 100644 index 0000000..b09b105 --- /dev/null +++ b/src/commands/admin/fund.ts @@ -0,0 +1,40 @@ +import { stripIndents } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { confirm, ensureDecimal, resolveUser } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class FundCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'fund', + description: "Fund a user's account with some LBC.", + category: 'Admin', + aliases: ['fundacc', 'fundaccount'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['fund @user 2.0'], + usage: ' ' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + const discordID = resolveUser(ctx.args[0]); + if (!discordID) return "That Discord user isn't valid."; + const account = await this.lbryx.ensureAccount(discordID, false); + if (!account.id) return 'That user does not have an account.'; + const amount = ensureDecimal(ctx.args[1]); + if (!amount) return 'You must give a numeric amount of LBC to send!'; + + if (!(await confirm(ctx, `Are you sure you want to fund this account ${amount} LBC?`))) return; + + const transaction = await this.lbry.accountFund({ amount, to_account: account.id, broadcast: true }); + this.log('info', `Funded account ${account.id} ${amount}`, transaction); + return stripIndents` + Successfully funded account! + 🔗 https://explorer.lbry.com/tx/${transaction.txid} + `; + } +} diff --git a/src/commands/admin/fundall.ts b/src/commands/admin/fundall.ts new file mode 100644 index 0000000..4aa0706 --- /dev/null +++ b/src/commands/admin/fundall.ts @@ -0,0 +1,77 @@ +import { CommandContext, DexareClient } from 'dexare'; +import { confirm, ensureDecimal, wait } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class FundAllCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'fundall', + description: 'Fund all users in the database with some LBC.', + category: 'Admin', + userPermissions: ['lbry.admin'], + metadata: { + examples: ['fundall 2.0'], + usage: '' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + const amount = ensureDecimal(ctx.args[0]); + if (!amount) return 'You must give a numeric amount of LBC to send!'; + + await this.lbryx.sync(); + const pairs = this.lbryx.getIDs(); + + // empty DB population + if (pairs.length <= 0) { + const procMsg = await ctx.reply('No users in the database, creating accounts...'); + await this.client.startTyping(ctx.channel.id); + const rolesConfig: string | string[] = this.client.config.curatorRoles; + const curatorRoles = Array.isArray(rolesConfig) ? rolesConfig : [rolesConfig]; + const members = await this.client.bot.guilds.get(this.client.config.guildID)!.fetchMembers(); + for (const member of members) { + if (curatorRoles.map((r) => member.roles.includes(r)).includes(true)) { + const account = await this.lbryx.ensureAccount(member.id); + pairs.push([member.id, account.id]); + } + } + await wait(5000); + this.client.stopTyping(ctx.channel.id); + await procMsg.delete().catch(() => {}); + } + + if (!(await confirm(ctx, `Are you sure you want to fund **all** accounts ${amount} LBC?`))) return; + + await this.client.startTyping(ctx.channel.id); + const resultLines = []; + let funded = 0, + errored = 0; + for (const [discordID, accountID] of pairs) { + try { + const transaction = await this.lbry.accountFund({ to_account: accountID, amount, broadcast: true }); + console.info('Funded account', accountID, transaction.txid); + resultLines.push(`${discordID} - https://explorer.lbry.com/tx/${transaction.txid}`); + funded++; + } catch (e) { + this.log('info', 'Failed to fund account', accountID, e); + resultLines.push(`${discordID} ! ${e.toString()}`); + errored++; + } + await wait(3000); + } + this.client.stopTyping(ctx.channel.id); + + await ctx.reply( + errored + ? `Failed to fund ${errored} accounts! (${funded} funded)` + : `Successfully funded ${funded} account(s)!`, + { + name: 'result.txt', + file: Buffer.from(resultLines.join('\n'), 'utf8') + } + ); + } +} diff --git a/src/commands/admin/listall.ts b/src/commands/admin/listall.ts new file mode 100644 index 0000000..7e7a9ad --- /dev/null +++ b/src/commands/admin/listall.ts @@ -0,0 +1,51 @@ +import { stripIndents } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { Balance } from '../../modules/lbry/types'; +import { GeneralCommand } from '../../util/abstracts'; +import { paginate } from '../../util/pager'; + +export default class ListAllCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'listall', + description: 'List all users in the database.', + category: 'Admin', + userPermissions: ['lbry.admin'], + metadata: { + examples: ['listall', 'listall 2'], + usage: '[page]' + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + const pairs = this.lbryx.getIDs(); + const walletMap = new Map(); + + for (const [, accountID] of pairs) { + try { + const wallet = await this.lbry.accountBalance({ account_id: accountID }); + walletMap.set(accountID, wallet); + } catch (e) {} + } + + await paginate(ctx, { + title: 'Users', + items: pairs.map(([discordID, accountID]) => { + const bal = walletMap.get(accountID); + return stripIndents` + > <@${discordID}> - \`${accountID}\` + > ${ + bal + ? `${bal.available} available, ${bal.reserved_subtotals.supports} staked.` + : 'Wallet Unavailable' + } + `; + }), + itemSeparator: '\n\n', + startPage: parseInt(ctx.args[0]) + }); + } +} diff --git a/src/commands/admin/sync.ts b/src/commands/admin/sync.ts new file mode 100644 index 0000000..1429061 --- /dev/null +++ b/src/commands/admin/sync.ts @@ -0,0 +1,23 @@ +import { DexareClient } from 'dexare'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class SyncCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'sync', + description: 'Syncs ID pairs with the SDK.', + category: 'Admin', + userPermissions: ['lbry.admin'], + metadata: { + examples: ['sync'] + } + }); + + this.filePath = __filename; + } + + async run() { + const synced = await this.lbryx.sync(); + return `Synced ${synced.toLocaleString()} new pairs.`; + } +} diff --git a/src/commands/admin/withdraw.ts b/src/commands/admin/withdraw.ts new file mode 100644 index 0000000..581cdd6 --- /dev/null +++ b/src/commands/admin/withdraw.ts @@ -0,0 +1,53 @@ +import { oneLine, stripIndents } from 'common-tags'; +import { CommandContext, DexareClient } from 'dexare'; +import { confirm, ensureDecimal } from '../../util'; +import { GeneralCommand } from '../../util/abstracts'; + +export default class WithdrawCommand extends GeneralCommand { + constructor(client: DexareClient) { + super(client, { + name: 'withdraw', + description: 'Sends funds to an address from the master wallet.', + category: 'Admin', + aliases: ['wd'], + userPermissions: ['lbry.admin'], + metadata: { + examples: ['wd abcd1234 2.0'] + } + }); + + this.filePath = __filename; + } + + async run(ctx: CommandContext) { + const addr = ctx.args[0]; + if (!addr || !/^[\w]{34}$/.test(addr)) return 'The address is invalid!'; + const amount = ensureDecimal(ctx.args[1]); + if (!amount) return 'You must give a numeric amount of LBC to send!'; + + // Check if the balance is more than requested + const balance = await this.lbry.walletBalance(); + const availableBalance = parseFloat(balance.available); + if (parseFloat(amount) > availableBalance) + return 'There is not enough available LBC in the wallet to send that amount!'; + + // Send to wallet + if ( + !(await confirm( + ctx, + oneLine` + Are you sure you want to send ${amount} to \`${addr}\`? + *(remaining: ${availableBalance - parseFloat(amount)})* + ` + )) + ) + return; + + const transaction = await this.lbry.walletSend({ amount, addresses: addr }); + this.log('info', `Withdrew ${amount} from master wallet to ${addr}`, transaction); + return stripIndents` + Sent ${amount} LBC to \`${addr}\`. + 🔗 https://explorer.lbry.com/tx/${transaction.txid} + `; + } +}