diff --git a/config/_default.js b/config/_default.js index debcf4b..625d6b9 100644 --- a/config/_default.js +++ b/config/_default.js @@ -44,10 +44,12 @@ module.exports = { }, logger: { + // [string] The highest level to log from level: 'debug' }, cron: { + // [string] The folder to load crons from loadFolder: './src/crons' }, @@ -58,7 +60,9 @@ module.exports = { lbryx: { // [string?] Amount to auto-fund upon account creation - startingBalance: "" + startingBalance: "", + // [string?] The file to write pairs to + databasePath: "data/lbry.sqlite" }, wallet: { diff --git a/src/modules/lbry/types.ts b/src/modules/lbry/types.ts index d3d6372..3a4617b 100644 --- a/src/modules/lbry/types.ts +++ b/src/modules/lbry/types.ts @@ -301,7 +301,7 @@ export interface JSONRPCResponse { } export interface PaginatingResult { - items: T; + items: T[]; page: number; page_size: number; total_items: number; diff --git a/src/modules/lbryx.ts b/src/modules/lbryx.ts new file mode 100644 index 0000000..ef1e8e3 --- /dev/null +++ b/src/modules/lbryx.ts @@ -0,0 +1,248 @@ +import { DexareModule, DexareClient, BaseConfig } from 'dexare'; +import type { table as QuickDBTable } from 'quick.db'; +import * as LBRY from './lbry/types'; +import { CurateConfig } from '../bot'; +import LBRYModule from './lbry'; +import WalletModule from './wallet'; +import { wait } from '../util'; + +export interface LBRYXConfig extends BaseConfig { + lbry?: LBRYXModuleOptions; +} + +export interface LBRYXModuleOptions { + startingBalance?: string; + databasePath: string; +} + +export interface EnsureAccountResult { + id: string; + txid?: string; + new?: boolean; +} + +export default class LBRYXModule> extends DexareModule { + db: QuickDBTable; + defaultAccount?: string; + + constructor(client: T) { + super(client, { + name: 'lbry', + description: 'Helper module and database for the LBRY Curate bot' + }); + + this.filePath = __filename; + + const qdb = require('quick.db')(this.config.databasePath || 'data/lbry.sqlite'); + this.db = qdb.table('pairs'); + } + + /* #region aliases */ + get config() { + return this.client.config.lbryx; + } + + get lbry() { + return this.client.modules.get('lbry')! as LBRYModule; + } + + get wallet() { + return this.client.modules.get('wallet')! as WalletModule; + } + /* #endregion */ + + /* #region database */ + /** + * Get a LBRY account ID from a Discord ID. + * @param id The Discord ID to use + */ + getID(id: string) { + return this.db.get(id) as string | null; + } + + /** + * Link a LBRY account ID to a Discord ID. + * @param id The Discord ID to use + * @param accountID The LBRY account ID to use + */ + setID(id: string, accountID: string) { + return this.db.set(id, accountID) as string | null; + } + + /** + * Remove an ID association. + * @param id The Discord ID to use + */ + removeID(id: string) { + return this.db.delete(id); + } + + /** + * Get all ID pairs in the database. + */ + getIDs() { + return this.db.all().map((row) => [row.ID, row.data]) as [string, string][]; + } + + /** + * Sync ID associations to the database. + */ + async sync() { + const accounts = await this.lbry.accountList({ page_size: await this.getAccountCount() }); + + let syncedAccounts = 0; + for (const account of accounts.items) { + if (/\d{17,19}/.test(account.name)) { + if (this.getID(account.name)) continue; + this.setID(account.name, account.id); + syncedAccounts++; + } + } + + return syncedAccounts; + } + /* #endregion */ + + /* #region count */ + /** + * Get the amount of accounts in the SDK. + */ + async getAccountCount() { + const response = await this.lbry.accountList({ page_size: 1 }); + return response.total_items; + } + + /** + * Get the amount of supports from an account. + * @param accountID The account ID to use + */ + async getSupportsCount(accountID: string) { + const response = await this.lbry.supportList({ account_id: accountID, page_size: 1 }); + return response.total_items; + } + /* #endregion */ + + /* #region account */ + /** + * Find an SDK account. + * @param fn The function to iterate with + */ + async findAccount(fn: (account: LBRY.Account) => boolean) { + const accounts = await this.lbry.accountList({ page_size: await this.getAccountCount() }); + return accounts.items.find(fn); + } + + /** + * Creates an SDK account for a Discord user. + * @param id The Discord ID to use + */ + async createAccount(id: string) { + this.logger.info('Creating account for user', id); + const account = await this.lbry.accountCreate({ account_name: id, single_key: true }); + this.setID(id, account.id); + this.logger.info('Created pair', id, account.id); + let transaction: LBRY.Transaction | null = null; + if (this.config.startingBalance) { + transaction = await this.lbry.accountFund({ + to_account: account.id, + amount: this.config.startingBalance, + broadcast: true + }); + this.logger.info('Funded account', account.id, transaction.txid); + } + return { account, transaction }; + } + + /** + * Get the account ID of the default account. + * For trusted member use. + */ + async getDefaultAccount() { + if (this.defaultAccount) return this.defaultAccount; + const account = await this.findAccount((account) => account.is_default); + this.defaultAccount = account!.id; + return account!.id; + } + + /** + * Finds an account ID or creates one. + * @param discordID The Discord ID to use + * @param create Whether to create the account if not created already + */ + async ensureAccount(discordID: string, create = true): Promise { + // Check SQLite + const id = this.getID(discordID); + if (id) return { id }; + + // Check accounts via SDK + const foundAccount = await this.findAccount((account) => account.name === discordID); + if (foundAccount) { + this.setID(discordID, foundAccount.id); + return { id: foundAccount.id }; + } + + // Create account if not found + if (create) { + const account = await this.createAccount(discordID); + return { + id: account.account.id, + txid: account.transaction?.txid, + new: true + }; + } else return { id: '' }; + } + + /** + * Deletes an account from the SDK and database. + * @param discordID The Discord ID to use + * @param id The LBRY account ID to use + */ + async deleteAccount(discordID: string, id: string) { + // Backup the wallet before doing any delete function + try { + this.wallet.backup(); + } catch (err) { + this.logger.error('Error occurred while backing up wallet file!', err); + throw err; + } + + // Abandon supports + await this.abandonAllClaims(id); + + // Take out funds from account + const balance = await this.lbry.accountBalance({ account_id: id }); + let amount = balance.total; + while (parseFloat(amount) >= 2) { + await this.lbry.accountFund({ from_account: id, everything: true, amount }); + const newBalance = await this.lbry.accountBalance({ account_id: id }); + amount = newBalance.total; + await wait(3000); + } + + // Remove account from SDK & SQLite + await this.lbry.accountRemove({ account_id: id }); + this.removeID(discordID); + } + /* #endregion */ + + /** + * Abandon all claims from a LBRY account. + * @param accountID The LBRY account ID to use + */ + async abandonAllClaims(accountID: string) { + const supportsCount = await this.getSupportsCount(accountID); + if (supportsCount <= 0) return; + + const supports = await this.lbry.supportList({ + account_id: accountID, + page_size: supportsCount, + no_totals: true + }); + this.logger.info(`Abandoning claims for ${accountID} (${supportsCount})`); + for (const support of supports.items) { + await this.lbry.supportAbandon({ claim_id: support.claim_id, account_id: accountID }); + await wait(3000); + } + return { count: supports.items.length }; + } +} diff --git a/src/util/abstracts.ts b/src/util/abstracts.ts index 552b72a..3fefa4a 100644 --- a/src/util/abstracts.ts +++ b/src/util/abstracts.ts @@ -1,6 +1,7 @@ import { oneLine } from 'common-tags'; import { ClientEvent, CommandContext, DexareCommand, PermissionNames } from 'dexare'; import LBRYModule from '../modules/lbry'; +import LBRYXModule from '../modules/lbryx'; import WalletModule from '../modules/wallet'; export abstract class GeneralCommand extends DexareCommand { @@ -9,7 +10,7 @@ export abstract class GeneralCommand extends DexareCommand { } get lbryx() { - return this.client.modules.get('lbryx')! as LBRYModule; + return this.client.modules.get('lbryx')! as LBRYXModule; } get wallet() {