Start rewriting to dexare

This commit is contained in:
Snazzah 2021-06-23 21:55:35 -05:00
parent d1a7cbead6
commit 5d4037f90a
No known key found for this signature in database
GPG key ID: 5E71D54F3D86282E
61 changed files with 1824 additions and 4211 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View file

@ -1 +1,3 @@
config/ config/
src-old/
node_modules/

37
.eslintrc.js Normal file
View file

@ -0,0 +1,37 @@
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
},
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
globals: {
NodeJS: true,
BigInt: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {
'prettier/prettier': 'warn',
'no-cond-assign': [2, 'except-parens'],
'no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': 1,
'no-empty': [
'error',
{
allowEmptyCatch: true
}
],
'prefer-const': [
'warn',
{
destructuring: 'all'
}
],
'spaced-comment': 'warn'
}
};

View file

@ -1,79 +0,0 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"array-bracket-spacing": [
"warn",
"never"
],
"computed-property-spacing": "warn",
"indent": [
"warn",
2
],
"keyword-spacing": [
"warn",
{
"before": true,
"after": true
}
],
"max-len": [
"warn",
{
"code": 110,
"ignoreComments": true,
"ignoreUrls": true
}
],
"no-cond-assign": [
2,
"except-parens"
],
"no-use-before-define": [
2,
{
"functions": false,
"classes": false,
"variables": false
}
],
"new-cap": 0,
"no-caller": 2,
"no-undef": 2,
"no-unused-vars": 1,
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"no-console": "off",
"no-multi-spaces": "warn",
"prefer-const": [
"warn",
{
"destructuring": "all"
}
],
"quotes": [
"warn",
"single"
],
"semi": [
"warn",
"always"
],
"spaced-comment": "warn",
"space-infix-ops": "warn"
}
}

13
.gitignore vendored
View file

@ -1,6 +1,11 @@
node_modules node_modules
config/default.js config/*
config/production.js !config/_default.js
config/curate.sqlite *.sqlite
package-lock.json package-lock.json
.idea/ yarn.lock
yarn-error.log
.idea/
.vscode/*
!.vscode/extensions.json
src-old/

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "none",
"printWidth": 110
}

View file

@ -9,7 +9,6 @@ This bot allows the community of LBRY to support eachother through the [LBRY Fou
* Pull the repo * Pull the repo
* Install [Node.JS LTS](https://nodejs.org/) (Currently Node v12.x) * Install [Node.JS LTS](https://nodejs.org/) (Currently Node v12.x)
* Install [Yarn](https://yarnpkg.com/) (`npm install yarn -g`) * Install [Yarn](https://yarnpkg.com/) (`npm install yarn -g`)
* Install [Redis](https://redis.io/) ([quickstart](https://redis.io/topics/quickstart))
* Install LBRY-SDK * Install LBRY-SDK
* Set your NODE_ENV (Node environment) Environment Variable to Production (`EXPORT NODE_ENV=production`) * Set your NODE_ENV (Node environment) Environment Variable to Production (`EXPORT NODE_ENV=production`)
* In the `config/` folder, copy `_default.js` to `production.js` and edit the config as needed * In the `config/` folder, copy `_default.js` to `production.js` and edit the config as needed

View file

@ -1,56 +1,71 @@
module.exports = { module.exports = {
// [string] The token for the bot
token: "",
// [string] The prefix for the bot
prefix: "!",
// [Array<string>] An array of elevated IDs, giving them access to developer commands
elevated: [],
// [string] The path where the commands will be found // [string] The path where the commands will be found
commandsPath: "./src/commands", commandsPath: "./src/commands",
// [boolean] Whether debug logs will be shown
debug: false, // Dexare config
// [number] The main embed color (#ffffff -> 0xffffff) dexare: {
embedColor: 0x15521c, // [string] The token for the bot
// [string|Array<string>] The role ID(s) for curator roles token: "",
curatorRoleID: "", // [string] The prefix for the bot
// [string|Array<string>] The role ID(s) for trusted roles prefix: "c!",
trustedRoleID: "", // [boolean?] Whether to use the bots mention as a prefix
// [string|Array<string>] The role ID(s) for admin roles mentionPrefix: true,
adminRoleID: "", // [Array<string>] An array of elevated IDs, giving them access to developer commands
// [string] guild_id elevated: [],
guildID: "", // [number] The main embed color (#ffffff -> 0xffffff)
// [string] sdk_url embedColor: 0x15521c,
sdkURL: "", // [string|Array<string>] The role ID(s) for curator roles
// [string] The ABSOLUTE path to the main wallet file to back up curatorRoles: "",
walletPath: "~/.lbryum/wallets/default_wallet", // [string|Array<string>] The role ID(s) for trusted roles
// [string] The ABSOLUTE path folder to store wallet backups after every deletion trustedRoles: "",
walletBackupFolder: "~/.lbryum_backup/", // [string|Array<string>] The role ID(s) for admin roles
// [string] Amount to auto-fund upon account creation adminRoles: "",
startingBalance: "", // [string] The ID of the main Discord guild
// [Object] Eris client options (https://abal.moe/Eris/docs/Client) guildID: "",
discordConfig: {
autoreconnect: true,
allowedMentions: { // [Object] Eris client options (https://abal.moe/Eris/docs/Client)
everyone: false, erisConfig: {
roles: false, autoreconnect: true,
users: true allowedMentions: {
everyone: false,
roles: false,
users: true
},
maxShards: "auto",
messageLimit: 0,
intents: [
"guilds",
"guildMessages",
"guildMessageReactions",
"directMessages",
"directMessageReactions"
]
}, },
maxShards: "auto",
messageLimit: 0, logger: {
intents: [ level: 'debug'
"guilds", },
"guildEmojis",
"guildMessages", cron: {
"guildMessageReactions", loadFolder: './src/crons'
"directMessages", },
"directMessageReactions"
] lbry: {
}, // [string] The SDK url to request from
// [Object] Redis config sdkURL: ""
redis: { },
host: "localhost",
port: 6379, lbryx: {
password: "", // [string?] Amount to auto-fund upon account creation
prefix: "lbrycurate:" startingBalance: ""
},
wallet: {
// [string] The ABSOLUTE path to the main wallet file to back up
path: "~/.lbryum/wallets/default_wallet",
// [string] The ABSOLUTE path folder to store wallet backups after every deletion
backupFolder: "~/.lbryum_backup/",
}
} }
} }

View file

@ -1,29 +1,44 @@
{ {
"name": "lbry-curate", "name": "lbry-curate",
"version": "0.1.1", "version": "1.0.0",
"description": "Support the LBRY Community through Discord!", "description": "Support the LBRY Community through Discord!",
"main": "src/bot.js", "main": "dist/index.js",
"scripts": { "scripts": {
"start": "node src/bot.js", "start": "cd dist && node index.js",
"eslint": "eslint ./src", "start:prod": "cd dist && NODE_ENV=production node index.js",
"eslint:fix": "eslint ./src --fix" "build": "tsc",
"dev": "devScript",
"lint": "npx eslint --ext .ts ./src",
"lint:fix": "npx eslint --ext .ts ./src --fix"
},
"devScript": {
"depCheck": false
}, },
"dependencies": { "dependencies": {
"abort-controller": "^3.0.0", "@dexare/cron": "^1.0.0",
"cat-loggr": "^1.2.2", "@dexare/logger": "^1.0.0",
"common-tags": "^1.8.0",
"config": "^3.3.6", "config": "^3.3.6",
"eris": "^0.15.1", "dexare": "^2.0.1",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"fuzzy": "^0.1.3", "quick.db": "^7.1.3",
"moment": "^2.29.1", "steno": "^2.0.0"
"node-fetch": "^2.6.1",
"redis": "^3.1.2",
"require-reload": "^0.2.2",
"sequelize": "^6.6.2",
"sqlite3": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.28.0" "@types/common-tags": "^1.8.0",
"@types/config": "^0.0.38",
"@types/cron": "^1.7.2",
"@types/needle": "^2.5.1",
"@types/node": "^15.12.4",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.3.1",
"ts-devscript": "^3.0.5",
"ts-node": "^10.0.0",
"typescript": "^4.3.4"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -2,8 +2,8 @@
"apps": [ "apps": [
{ {
"name": "LBRYCurate", "name": "LBRYCurate",
"script": "node", "script": "yarn",
"args": "src/bot.js" "args": "start:prod"
} }
] ]
} }

View file

@ -1,151 +0,0 @@
const Eris = require('eris');
const Database = require('./database');
const EventHandler = require('./events');
const CommandLoader = require('./commandloader');
const MessageAwaiter = require('./messageawaiter');
const SQLiteDB = require('./sqlitedb');
const path = require('path');
const CatLoggr = require('cat-loggr');
const config = require('config');
const LBRY = require('./structures/LBRY');
class CurateBot extends Eris.Client {
constructor({ packagePath, mainDir } = {}) {
// Initialization
const pkg = require(packagePath || `${mainDir}/package.json`);
super(config.token, JSON.parse(JSON.stringify(config.discordConfig)));
this.dir = mainDir;
this.pkg = pkg;
this.logger = new CatLoggr({
level: config.debug ? 'debug' : 'info',
levels: [
{ name: 'fatal', color: CatLoggr._chalk.red.bgBlack, err: true },
{ name: 'error', color: CatLoggr._chalk.black.bgRed, err: true },
{ name: 'warn', color: CatLoggr._chalk.black.bgYellow, err: true },
{ name: 'init', color: CatLoggr._chalk.black.bgGreen },
{ name: 'webserv', color: CatLoggr._chalk.black.bgBlue },
{ name: 'info', color: CatLoggr._chalk.black.bgCyan },
{ name: 'assert', color: CatLoggr._chalk.cyan.bgBlack },
{ name: 'poster', color: CatLoggr._chalk.yellow.bgBlack },
{ name: 'debug', color: CatLoggr._chalk.magenta.bgBlack, aliases: ['log', 'dir'] },
{ name: 'limiter', color: CatLoggr._chalk.gray.bgBlack },
{ name: 'fileload', color: CatLoggr._chalk.white.bgBlack }
]
});
this.logger.setGlobal();
this.typingIntervals = new Map();
// Events
this.on('ready', () => console.info('All shards ready.'));
this.on('disconnect', () => console.warn('All shards Disconnected.'));
this.on('reconnecting', () => console.warn('Reconnecting client.'));
this.on('debug', message => console.debug(message));
// Shard Events
this.on('connect', id => console.info(`Shard ${id} connected.`));
this.on('error', (error, id) => console.error(`Error in shard ${id}`, error));
this.on('hello', (_, id) => console.debug(`Shard ${id} recieved hello.`));
this.on('warn', (message, id) => console.warn(`Warning in Shard ${id}`, message));
this.on('shardReady', id => console.info(`Shard ${id} ready.`));
this.on('shardResume', id => console.warn(`Shard ${id} resumed.`));
this.on('shardDisconnect', (error, id) => console.warn(`Shard ${id} disconnected`, error));
// SIGINT & uncaught exceptions
process.once('uncaughtException', async err => {
console.error('Uncaught Exception', err.stack);
await this.dieGracefully();
process.exit(0);
});
process.once('SIGINT', async () => {
console.info('Caught SIGINT');
await this.dieGracefully();
process.exit(0);
});
console.init('Client initialized');
}
/**
* Creates a promise that resolves on the next event
* @param {string} event The event to wait for
*/
waitTill(event) {
return new Promise(resolve => this.once(event, resolve));
}
/**
* Starts the processes and log-in to Discord.
*/
async start() {
// Redis
this.db = new Database(this);
await this.db.connect(config.redis);
this.sqlite = new SQLiteDB(this);
// Discord
await this.connect();
await this.waitTill('ready');
this.editStatus('online', {
name: `${config.prefix}help`,
type: 3,
});
// Commands
this.cmds = new CommandLoader(this, path.join(this.dir, config.commandsPath));
this.cmds.reload();
this.cmds.preloadAll();
// Events
this.messageAwaiter = new MessageAwaiter(this);
this.eventHandler = new EventHandler(this);
this.lbry = new LBRY(this);
}
/**
* KIlls the bot
*/
dieGracefully() {
return super.disconnect();
}
// Typing
/**
* Start typing in a channel
* @param {Channel} channel The channel to start typing in
*/
async startTyping(channel) {
if (this.isTyping(channel)) return;
await channel.sendTyping();
this.typingIntervals.set(channel.id, setInterval(() => {
channel.sendTyping().catch(() => this.stopTyping(channel));
}, 5000));
}
/**
* Whether the bot is currently typing in a channel
* @param {Channel} channel
*/
isTyping(channel) {
return this.typingIntervals.has(channel.id);
}
/**
* Stops typing in a channel
* @param {Channel} channel
*/
stopTyping(channel) {
if (!this.isTyping(channel)) return;
const interval = this.typingIntervals.get(channel.id);
clearInterval(interval);
this.typingIntervals.delete(channel.id);
}
}
const Bot = new CurateBot({ mainDir: path.join(__dirname, '..') });
Bot.start().catch(e => {
Bot.logger.error('Failed to start bot! Exiting in 10 seconds...');
console.error(e);
setTimeout(() => process.exit(0), 10000);
});

97
src/bot.ts Normal file
View file

@ -0,0 +1,97 @@
import { DexareClient, BaseConfig, PermissionObject } from 'dexare';
import config from 'config';
import path from 'path';
import chalk from 'chalk';
import LoggerModule, { LoggerModuleOptions } from '@dexare/logger';
import CronModule, { CronModuleOptions } from '@dexare/cron';
import WalletModule, { WalletModuleOptions } from './modules/wallet';
import LBRYModule, { LBRYModuleOptions } from './modules/lbry';
import LBRYXModule, { LBRYXModuleOptions } from './modules/lbryx';
export const PRODUCTION = process.env.NODE_ENV === 'production';
export interface CurateConfig extends BaseConfig {
prefix: string | string[];
mentionPrefix: boolean;
guildID: string;
embedColor: number;
trustedRoles: string | string[];
curatorRoles: string | string[];
adminRoles: string | string[];
logger: LoggerModuleOptions;
wallet: WalletModuleOptions;
lbry: LBRYModuleOptions;
lbryx: LBRYXModuleOptions;
cron?: CronModuleOptions;
}
export const client = new DexareClient(config.get('dexare') as CurateConfig);
client.loadModules(LoggerModule, WalletModule, LBRYModule, LBRYXModule, CronModule);
client.commands.registerDefaults(['eval', 'kill', 'exec', 'load', 'unload', 'reload', 'help']);
client.commands.registerFromFolder(path.join(config.get('commandsPath' as string)));
/* #region perms */
export function rolePermissionCheck(...roles: (string | string[])[]) {
return (object: PermissionObject) => {
if (!object.member) return false;
const roleIDs: string[] = [];
roles.map((role) => roleIDs.concat(Array.isArray(role) ? role : [role]));
const member = client.bot.guilds.get(client.config.guildID)!.members.get(object.user.id)!;
// elevated user bypass
if (client.config.elevated) {
if (Array.isArray(client.config.elevated)) return client.config.elevated.includes(object.user.id);
else if (client.config.elevated === object.user.id) return true;
}
return roleIDs.map((r) => member.roles.includes(r)).includes(true);
};
}
client.permissions.register('lbry.curator', rolePermissionCheck(client.config.curatorRoles));
client.permissions.register('lbry.trusted', rolePermissionCheck(client.config.trustedRoles));
client.permissions.register('lbry.admin', rolePermissionCheck(client.config.adminRoles));
client.permissions.register(
'lbry.curatorOrAdmin',
rolePermissionCheck(client.config.curatorRoles, client.config.adminRoles)
);
client.permissions.register(
'lbry.trustedOrAdmin',
rolePermissionCheck(client.config.trustedRoles, client.config.adminRoles)
);
/* #endregion */
const logger = client.modules.get('logger') as any as LoggerModule<any>;
logger.moduleColors.lbry = chalk.black.bgCyan;
logger.moduleColors.lbryx = chalk.red.bgCyan;
logger.moduleColors.lbrybot = chalk.cyan.bgBlack;
logger.moduleColors.wallet = chalk.black.bgKeyword('brown');
process.once('SIGINT', async () => {
client.emit('logger', 'warn', 'sys', ['Caught SIGINT']);
await client.disconnect();
process.exit(0);
});
process.once('beforeExit', async () => {
client.emit('logger', 'warn', 'sys', ['Exiting....']);
await client.disconnect();
process.exit(0);
});
export async function connect() {
await client.connect();
client.bot.shards.forEach((shard) =>
shard.editStatus(
'online',
PRODUCTION ? { name: 'the blockchain | c!help', type: 5 } : { name: 'logs | c!help', type: 3 }
)
);
}
export async function disconnect() {
await client.disconnect();
}

View file

@ -1,101 +0,0 @@
const fs = require('fs');
const path = require('path');
const reload = require('require-reload')(require);
const config = require('config');
module.exports = class CommandLoader {
constructor(client, cPath) {
this.commands = [];
this.path = path.resolve(cPath);
this.client = client;
}
/**
* Loads commands from a folder
* @param {String} folderPath
*/
iterateFolder(folderPath) {
const files = fs.readdirSync(folderPath);
files.map(file => {
const filePath = path.join(folderPath, file);
const stat = fs.lstatSync(filePath);
if (stat.isSymbolicLink()) {
const realPath = fs.readlinkSync(filePath);
if (stat.isFile() && file.endsWith('.js')) {
this.load(realPath);
} else if (stat.isDirectory()) {
this.iterateFolder(realPath);
}
} else if (stat.isFile() && file.endsWith('.js'))
this.load(filePath);
else if (stat.isDirectory())
this.iterateFolder(filePath);
});
}
/**
* Loads a command
* @param {string} commandPath
*/
load(commandPath) {
console.fileload('Loading command', commandPath);
const cls = reload(commandPath);
const cmd = new cls(this.client);
cmd.path = commandPath;
this.commands.push(cmd);
return cmd;
}
/**
* Reloads all commands
*/
reload() {
this.commands = [];
this.iterateFolder(this.path);
}
/**
* Gets a command based on it's name or alias
* @param {string} name The command's name or alias
*/
get(name) {
let cmd = this.commands.find(c => c.name === name);
if (cmd) return cmd;
this.commands.forEach(c => {
if (c.options.aliases.includes(name)) cmd = c;
});
return cmd;
}
/**
* Preloads a command
* @param {string} name The command's name or alias
*/
preload(name) {
if (!this.get(name)) return;
this.get(name)._preload();
}
/**
* Preloads all commands
*/
preloadAll() {
this.commands.forEach(c => c._preload());
}
/**
* Processes the cooldown of a command
* @param {Message} message
* @param {Command} command
*/
async processCooldown(message, command) {
if (config.elevated.includes(message.author.id)) return true;
const now = Date.now() - 1;
const cooldown = command.cooldownAbs;
let userCD = await this.client.db.hget(`cooldowns:${message.author.id}`, command.name) || 0;
if (userCD) userCD = parseInt(userCD);
if (userCD + cooldown > now) return false;
await this.client.db.hset(`cooldowns:${message.author.id}`, command.name, now);
return true;
}
};

View file

@ -1,58 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class AbaondonAll extends Command {
get name() { return 'abandonall'; }
get _options() { return {
aliases: ['abanall', 'dropall'],
permissions: ['admin'],
minimumArgs: 0
}; }
// @TODO: Refactor this command to be able to abandon all supports on the bot.
async exec(message, { args }) {
if (args.length) {
const discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, discordID, false);
if (!account.accountID)
return message.channel.createMessage('That user does not have an account.');
const supportsCount = await Util.LBRY.getSupportsCount(this.client, account.accountID);
if (supportsCount <= 0)
return message.channel.createMessage('That user does not have any supports.');
if (!await this.client.messageAwaiter.confirm(message, {
header:
`Are you sure you want to abandon **all supports** from that account? *(${
supportsCount.toLocaleString()} support[s])*`
})) return;
await Util.LBRY.abandonAllClaims(this.client, account.accountID);
return message.channel.createMessage(`Abandoned ${supportsCount.toLocaleString()} claim(s).`);
} else {
if (!await this.client.messageAwaiter.confirm(message, {
header: 'Are you sure you want to abandon **all supports** from **all accounts**?'
})) return;
await this.client.startTyping(message.channel);
await Util.LBRY.syncPairs(this.client);
const pairs = await this.client.sqlite.getAll();
let count = 0;
for (let i = 0, len = pairs.length; i < len; i++) {
const pair = pairs[i];
const result = await Util.LBRY.abandonAllClaims(this.client, pair.lbryID);
count += result.count;
}
this.client.stopTyping(message.channel);
return message.channel.createMessage(`Abandoned ${count.toLocaleString()} claim(s).`);
}
}
get metadata() { return {
category: 'Admin',
description: 'Abandons all supports of the bot or of a given account.',
usage: '[id|@mention]'
}; }
};

View file

@ -1,28 +0,0 @@
const Command = require('../../structures/Command');
const config = require('config');
module.exports = class AdminBalance extends Command {
get name() { return 'adminbalance'; }
get _options() { return {
aliases: ['abal', 'adminbal'],
permissions: ['admin']
}; }
async exec(message) {
const response = await this.client.lbry.walletBalance();
const wallet = await response.json();
if (await this.handleResponse(message, response, wallet)) return;
return message.channel.createMessage({ embed: {
color: config.embedColor,
description: `**Available:** ${wallet.result.available} LBC\n\n` +
`Reserved in Supports: ${wallet.result.reserved_subtotals.supports} LBC\n` +
`Total: ${wallet.result.total} LBC`
} });
}
get metadata() { return {
category: 'Admin',
description: 'Shows the master wallet balance.'
}; }
};

View file

@ -1,60 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
const GenericPager = require('../../structures/GenericPager');
module.exports = class AllSupports extends Command {
get name() { return 'allsupports'; }
get _options() { return {
aliases: ['asups', 'allsups'],
permissions: ['admin'],
minimumArgs: 0
}; }
async exec(message, { args }) {
let givenClaim;
if (args[0]) {
givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
}
await Util.LBRY.syncPairs(this.client);
const pairs = await this.client.sqlite.getAll();
if (pairs.length <= 0)
return message.channel.createMessage('No users found in the database.');
const allSupports = [];
for (const pair of pairs) {
const supportsCount = await Util.LBRY.getSupportsCount(this.client, pair.lbryID);
if (supportsCount <= 0) continue;
const supportsResponse = await this.client.lbry.listSupports({
accountID: pair.lbryID, page_size: supportsCount, claimID: givenClaim });
const supports = (await supportsResponse.json()).result.items;
for (const support of supports)
allSupports.push({
...support,
pair
});
}
if (allSupports.length <= 0)
return message.channel.createMessage('No supports found.');
const paginator = new GenericPager(this.client, message, {
items: allSupports,
header: `All supports${
givenClaim ? ` on claim \`${givenClaim}\`` : ''}`, itemTitle: 'Supports',itemsPerPage: 5,
display: item => `> ${item.name} \`${item.claim_id}\`\n> <@${item.pair.discordID}> ${item.amount} LBC\n`
});
return paginator.start(message.channel.id, message.author.id);
}
get metadata() { return {
category: 'Admin',
description: 'List all supports from all users.',
usage: '[claimID]'
}; }
};

View file

@ -1,42 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class DeleteAccount extends Command {
get name() { return 'deleteaccount'; }
get _options() { return {
aliases: ['del', 'delacc'],
permissions: ['admin'],
minimumArgs: 1
}; }
async exec(message, { args }) {
const discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, discordID, false);
if (account.accountID) {
const supportsCount = await Util.LBRY.getSupportsCount(this.client, account.accountID);
if (!await this.client.messageAwaiter.confirm(message, {
header:
`Are you sure you want to delete that account? *(${supportsCount.toLocaleString()} support[s])*`
})) return;
try {
await Util.LBRY.deleteAccount(this.client, discordID, account.accountID);
return message.channel.createMessage('Deleted account.');
} catch (e) {
return message.channel.createMessage(
'Failed to delete the account. An error most likely occured while backing up the wallet.' +
`\n\`\`\`\n${e.toString()}\`\`\``
);
}
} else
return message.channel.createMessage('That user does not have an account.');
}
get metadata() { return {
category: 'Admin',
description: 'Deletes a given Discord user\'s Curation account.',
usage: '<id|@mention>'
}; }
};

View file

@ -1,39 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class DeleteAll extends Command {
get name() { return 'deleteall'; }
get _options() { return {
aliases: ['delall'],
permissions: ['admin'],
minimumArgs: 0
}; }
async exec(message) {
await Util.LBRY.syncPairs(this.client);
const pairs = await this.client.sqlite.getAll();
if (!await this.client.messageAwaiter.confirm(message, {
header:
`Are you sure you want to delete **all** ${pairs.length} accounts?`
})) return;
for (const pair of pairs) {
try {
await Util.LBRY.deleteAccount(this.client, pair.discordID, pair.lbryID);
} catch (e) {
return message.channel.createMessage(
'Failed to delete an account. An error most likely occured while backing up the wallet.' +
`\n\`\`\`\n${e.toString()}\`\`\``
);
}
}
return message.channel.createMessage('Deleted all accounts.');
}
get metadata() { return {
category: 'Admin',
description: 'Deletes all accounts in the database.'
}; }
};

View file

@ -1,24 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Deposit extends Command {
get name() { return 'deposit'; }
get _options() { return {
aliases: ['dp'],
permissions: ['admin']
}; }
async exec(message) {
const account = await Util.LBRY.findSDKAccount(this.client, account => account.is_default);
const response = await this.client.lbry.listAddresses({ account_id: account.id });
const address = await response.json();
if (await this.handleResponse(message, response, address)) return;
return message.channel.createMessage(`Address: ${address.result.items[0].address}`);
}
get metadata() { return {
category: 'Admin',
description: 'Gets the address of the master wallet.'
}; }
};

View file

@ -1,38 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Fund extends Command {
get name() { return 'fund'; }
get _options() { return {
aliases: ['fundacc', 'fundaccount'],
permissions: ['admin'],
minimumArgs: 2
}; }
async exec(message, { args }) {
const givenAmount = Util.LBRY.ensureDecimal(args[1]);
if (!givenAmount)
return message.channel.createMessage('The second argument must be a numeric amount of LBC to send!');
const discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, discordID);
if (!await this.client.messageAwaiter.confirm(message, {
header: `Are you sure you want to fund this account? *(${givenAmount} LBC)*`
})) return;
const response = await this.client.lbry.fundAccount({ to: account.accountID, amount: givenAmount });
const transaction = await response.json();
console.info('Funded account', account.accountID, transaction.result.txid);
const txid = transaction.result.txid;
return message.channel.createMessage(`Successfully funded account! https://explorer.lbry.com/tx/${txid}`);
}
get metadata() { return {
category: 'Admin',
description: 'Funds a given Discord user\'s Curation account with the specified amount of LBC.',
usage: '<id|@mention> <amount>'
}; }
};

View file

@ -1,72 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
const config = require('config');
module.exports = class FundAll extends Command {
get name() { return 'fundall'; }
get _options() { return {
permissions: ['admin'],
minimumArgs: 1
}; }
async exec(message, { args }) {
const givenAmount = Util.LBRY.ensureDecimal(args[0]);
if (!givenAmount)
return message.channel.createMessage('The second argument must be a numeric amount of LBC to send!');
await Util.LBRY.syncPairs(this.client);
const pairs = await this.client.sqlite.getAll();
if (pairs.length <= 0) {
await this.client.startTyping(message.channel);
const curatorRoles = Array.isArray(config.curatorRoleID)
? config.curatorRoleID : [config.curatorRoleID];
const members = await this.client.guilds.get(config.guildID).fetchMembers();
for (const member of members) {
if (curatorRoles.map(r => member.roles.includes(r)).includes(true)) {
const account = await Util.LBRY.findOrCreateAccount(this.client, member.id);
pairs.push({ discordID: member.id, lbryID: account.accountID });
}
}
await Util.halt(5000);
this.client.stopTyping(message.channel);
}
if (!await this.client.messageAwaiter.confirm(message, {
header: `Are you sure you want to fund **all** accounts? *(${givenAmount} LBC)*`
})) return;
await this.client.startTyping(message.channel);
const resultLines = [];
let funded = 0,
errored = 0;
for (const pair of pairs) {
const response = await this.client.lbry.fundAccount({ to: pair.lbryID, amount: givenAmount });
await Util.halt(2000);
const transaction = await response.json();
if ('code' in transaction) {
console.info('Failed to fund account', pair.lbryID, transaction.code, transaction.message);
resultLines.push(`${pair.discordID} ! ${transaction.code} - ${transaction.message}`);
errored++;
} else {
console.info('Funded account', pair.lbryID, transaction.result.txid);
resultLines.push(`${pair.discordID} - https://explorer.lbry.com/tx/${transaction.result.txid}`);
funded++;
}
await Util.halt(2000);
}
this.client.stopTyping(message.channel);
return message.channel.createMessage(errored
? `Failed to fund ${errored} accounts! (${funded} funded)`
: `Successfully funded ${funded} account(s)!`, {
name: 'result.txt',
file: Buffer.from(resultLines.join('\n'), 'utf8')
});
}
get metadata() { return {
category: 'Admin',
description: 'Funds all users in the database a specified amount of LBC.',
usage: '<amount>'
}; }
};

View file

@ -1,49 +0,0 @@
const Command = require('../../structures/Command');
const GenericPager = require('../../structures/GenericPager');
module.exports = class ListAll extends Command {
get name() { return 'listall'; }
get _options() { return {
permissions: ['admin'],
minimumArgs: 0
}; }
async exec(message, { args }) {
const pairs = await this.client.sqlite.getAll();
if (pairs.length <= 0)
return message.channel.createMessage('No users found in the database.');
for (const pair of pairs) {
const response = await this.client.lbry.accountBalance(pair.lbryID);
const wallet = await response.json();
if (!wallet.code) {
pair.wallet_available = wallet.result.available;
pair.wallet_reserve = wallet.result.reserved_subtotals.supports;
pair.wallet_ok = true;
} else {
console.error([
'There was an error while retrieving the balance of an account.',
'This was likely caused by an old version of the Bot\'s SQLite database file. ' +
'Run the sync command to avoid this error!'
].join('\n'));
}
}
const paginator = new GenericPager(this.client, message, {
items: pairs, itemTitle: 'Users', itemsPerPage: 5,
display: pair => `> <@${pair.discordID}> - \`${pair.lbryID}\`\n` +
`> ${pair.wallet_ok
? `${pair.wallet_available} available, ${pair.wallet_reserve} staked.`
: 'Wallet Unavailable'}\n`
});
if (args[0])
paginator.toPage(args[0]);
return paginator.start(message.channel.id, message.author.id);
}
get metadata() { return {
category: 'Admin',
description: 'List all users in the database.',
usage: '[page]'
}; }
};

View file

@ -1,20 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Sync extends Command {
get name() { return 'sync'; }
get _options() { return {
permissions: ['admin']
}; }
async exec(message) {
const synced = await Util.LBRY.syncPairs(this.client);
return message.channel.createMessage(`Synced ${synced} new pairs.`);
}
get metadata() { return {
category: 'Admin',
description: 'Sync SDK-Discord pairs.'
}; }
};

View file

@ -1,45 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Withdraw extends Command {
get name() { return 'withdraw'; }
get _options() { return {
aliases: ['wd'],
permissions: ['admin'],
minimumArgs: 2
}; }
async exec(message, { args }) {
const amount = Util.LBRY.ensureDecimal(args[0]);
if (!amount)
return message.channel.createMessage('The first argument must be a numeric amount of LBC to send!');
// Check if the balance is more than requested
const balance = await this.client.lbry.walletBalance();
const balanceJSON = await balance.json();
if (await this.handleResponse(message, balance, balanceJSON)) return;
const availableBalance = parseFloat(balanceJSON.result.available);
if (parseFloat(amount) > availableBalance)
return message.channel.createMessage(
'There is not enough available LBC in the wallet to send that amount!');
// Send to wallet
if (!await this.client.messageAwaiter.confirm(message, {
header: `Are you sure you want to send ${amount} to \`${args[1]}\`? ` +
`*(remaining: ${availableBalance - parseFloat(amount)})*`
})) return;
const response = await this.client.lbry.sendToWallet({ amount, to: args[1] });
const transaction = await response.json();
if (await this.handleResponse(message, response, transaction)) return;
console.debug('withdrew from master wallet', transaction);
return message.channel.createMessage(`Sent ${amount} LBC to ${args[1]}.\n` +
`https://explorer.lbry.com/tx/${transaction.result.txid}`);
}
get metadata() { return {
category: 'Admin',
description: 'Sends funds to an address from the master wallet.',
usage: '<amount> <address>'
}; }
};

View file

@ -1,35 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Abandon extends Command {
get name() { return 'abandon'; }
get _options() { return {
aliases: ['aban', 'drop'],
permissions: ['curatorOrAdmin'],
minimumArgs: 1
}; }
async exec(message, { args }) {
const givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, message.author.id);
// Drop support
const response = await this.client.lbry.abandonSupport({
accountID: account.accountID, claimID: givenClaim });
const transaction = await response.json();
if (await this.handleResponse(message, response, transaction)) return;
const txid = transaction.result.txid;
return message.channel.createMessage(`Abandon successful! https://explorer.lbry.com/tx/${txid}`);
}
get metadata() { return {
category: 'Curator',
description: 'Abandons a support on a given claim.',
usage: '<claim>'
}; }
};

View file

@ -1,59 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
const config = require('config');
module.exports = class Balance extends Command {
get name() { return 'balance'; }
get _options() { return {
aliases: ['bal'],
permissions: ['curatorOrAdmin']
}; }
async exec(message, { args }) {
if (args.length) {
if (!Util.CommandPermissions.admin(this.client, message)) {
const admins = (Array.isArray(config.adminRoleID) ? config.adminRoleID : [config.adminRoleID])
.map(id => `"${this.client.guilds.get(config.guildID).roles.get(id).name}"`);
return message.channel.createMessage(
`You need to have the ${admins.join('/')} role(s) to see others balances!`);
}
const discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, discordID, false);
if (!account.accountID)
return message.channel.createMessage('That Discord user does not have an account.');
const response = await this.client.lbry.accountBalance(account.accountID);
const wallet = await response.json();
if (await this.handleResponse(message, response, wallet)) return;
return message.channel.createMessage({ embed: {
color: config.embedColor,
description: `<@${discordID}> has **${wallet.result.available}** LBC available.\n\n` +
`Reserved in Supports: ${wallet.result.reserved_subtotals.supports} LBC\n` +
`Total: ${wallet.result.total} LBC`
} });
} else {
const account = await Util.LBRY.findOrCreateAccount(this.client, message.author.id);
const response = await this.client.lbry.accountBalance(account.accountID);
const wallet = await response.json();
if (await this.handleResponse(message, response, wallet)) return;
return message.channel.createMessage({ embed: {
color: config.embedColor,
description: `You have **${wallet.result.available}** LBC available.\n\n` +
`Reserved in Supports: ${wallet.result.reserved_subtotals.supports} LBC\n` +
`Total: ${wallet.result.total} LBC` +
(account.newAccount ? '\n\n:warning: This account was just created. ' +
'Please wait a few seconds, and run the command again to get an accurate balance.' : '')
} });
}
}
get metadata() { return {
category: 'Curator',
description: 'Shows the user\'s account balance.'
}; }
};

View file

@ -1,52 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Support extends Command {
get name() { return 'support'; }
get _options() { return {
aliases: ['sup'],
permissions: ['curatorOrAdmin'],
minimumArgs: 2
}; }
async exec(message, { args }) {
const givenAmount = Util.LBRY.ensureDecimal(args[1]);
if (!givenAmount)
return message.channel.createMessage('The second argument must be a numeric amount of LBC to send!');
const givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
const account = await Util.LBRY.findOrCreateAccount(this.client, message.author.id);
if (account.newAccount) {
// Wait for the blockchain to complete the funding
await message.channel.sendTyping();
await Util.halt(3000);
}
// Get and check balance
const walletResponse = await this.client.lbry.accountBalance(account.accountID);
const wallet = await walletResponse.json();
if (await this.handleResponse(message, walletResponse, wallet)) return;
const balance = wallet.result.available;
if (parseFloat(givenAmount) > parseFloat(balance))
return message.channel.createMessage('You don\'t have enough LBC to do this!');
// Create support
const response = await this.client.lbry.createSupport({
accountID: account.accountID, claimID: givenClaim, amount: givenAmount });
const transaction = await response.json();
if (await this.handleResponse(message, response, transaction)) return;
const txid = transaction.result.txid;
return message.channel.createMessage(`Support successful! https://explorer.lbry.com/tx/${txid}`);
}
get metadata() { return {
category: 'Curator',
description: 'Support a given claim.',
usage: '<claim> <amount>'
}; }
};

View file

@ -1,61 +0,0 @@
const Command = require('../../structures/Command');
const GenericPager = require('../../structures/GenericPager');
const Util = require('../../util');
module.exports = class Supports extends Command {
get name() { return 'supports'; }
get _options() { return {
aliases: ['sups'],
permissions: ['curatorOrAdmin'],
minimumArgs: 0
}; }
async exec(message, { args }) {
let account, givenClaim, discordID;
if (args.length === 2) {
// Check for if claim ID and discord user is given
givenClaim = args[1];
if (!/^[a-f0-9]{40}$/.test(givenClaim))
return message.channel.createMessage('That Claim ID isn\'t valid.');
discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
account = await Util.LBRY.findOrCreateAccount(this.client, discordID, false);
} else if (args.length === 1) {
// Check for only if a discord user is given
discordID = Util.resolveToUserID(args[0]);
if (!discordID)
return message.channel.createMessage('That Discord user isn\'t valid.');
account = await Util.LBRY.findOrCreateAccount(this.client, discordID, false);
} else {
// Default to message author
account = await Util.LBRY.findOrCreateAccount(this.client, message.author.id);
}
if (!account.accountID)
return message.channel.createMessage('That Discord user does not have an account.');
const supportsCount = await Util.LBRY.getSupportsCount(this.client, account.accountID);
if (supportsCount <= 0)
return message.channel.createMessage('No supports found.');
const supportsResponse = await this.client.lbry.listSupports({
accountID: account.accountID, page_size: supportsCount, claimID: givenClaim });
console.debug(
`Displaying supports for ${
account.accountID}${givenClaim ? ` and claimID ${givenClaim}` : ''}, (${supportsCount})`);
const supports = (await supportsResponse.json()).result.items;
const paginator = new GenericPager(this.client, message, {
items: supports,
header: `All supports for <@${discordID || message.author.id}>${
givenClaim ? ` on claim \`${givenClaim}\`` : ''}`, itemTitle: 'Supports',itemsPerPage: 5,
display: item => `> ${item.name} \`${item.claim_id}\`\n> ${item.amount} LBC\n`
});
return paginator.start(message.channel.id, message.author.id);
}
get metadata() { return {
category: 'Curator',
description: 'Shows the user\'s list of supports.',
usage: '[id/@mention] [claimID]'
}; }
};

View file

@ -1,101 +0,0 @@
const Command = require('../structures/Command');
const Util = require('../util');
const config = require('config');
module.exports = class Help extends Command {
get name() { return 'help'; }
get _options() { return {
aliases: [
'?', 'h', 'commands', 'cmds', // English
'yardim', 'yardım', 'komutlar', // Turkish
'ayuda', // Spanish
'ajuda' // Catalan & Portuguese
],
permissions: ['embed'],
cooldown: 0,
}; }
exec(message, { args, }) {
if (args[0]) {
// Display help on a command
const command = this.client.cmds.get(args[0]);
if (!command)
return message.channel.createMessage(`The command \`${args[0]}\` could not be found.`);
else {
const embed = {
title: `${config.prefix}${command.name}`,
color: config.embedColor,
fields: [
{ name: '*Usage*',
value: `${config.prefix}${command.name}${
command.metadata.usage ?
` \`${command.metadata.usage}\`` : ''}` }
],
description: command.metadata.description
};
// Cooldown
if (command.options.cooldown)
embed.fields.push({
name: '*Cooldown*',
value: `${command.options.cooldown.toLocaleString()} second(s)`,
inline: false
});
// Aliases
if (command.options.aliases.length !== 0) embed.fields.push({
name: '*Alias(es)*',
value: command.options.aliases.map(a => `\`${a}\``).join(', ')
});
// Image
if (command.metadata.image)
embed.image = { url: command.metadata.image };
// Note
if (command.metadata.note)
embed.fields.push({
name: '*Note*',
value: command.metadata.note
});
return message.channel.createMessage({ embed });
}
} else {
// Display general help command
const embed = {
color: config.embedColor,
description: 'LBRY Curate',
footer: { text: `\`${config.prefix}help [command]\` for more info.` },
fields: []
};
// Populate categories
const categories = {};
this.client.cmds.commands.forEach(v => {
if (!v.options.listed && !config.elevated.includes(message.author.id)) return;
const string = v.name;
if (categories[v.metadata.category])
categories[v.metadata.category].push(string);
else categories[v.metadata.category] = [string];
});
// List categories
Util.keyValueForEach(categories, (k, v) => {
embed.fields.push({
name: `*${k}*`,
value: '```' + v.join(', ') + '```',
inline: true
});
});
return message.channel.createMessage({ embed });
}
}
get metadata() { return {
category: 'General',
description: 'Shows the help message and gives information on commands.',
usage: '[command]'
}; }
};

View file

@ -1,38 +0,0 @@
/* jshint evil: true */
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class AsyncEval extends Command {
get name() { return 'asynceval'; }
get _options() { return {
aliases: ['ae', 'aeval', 'aevaluate', 'asyncevaluate'],
permissions: ['elevated'],
listed: false,
}; }
// eslint-disable-next-line no-unused-vars
async exec(message, opts) {
try {
const start = Date.now();
const code = Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' ');
const result = await eval(`(async () => {${code}})()`);
const time = Date.now() - start;
return Util.Hastebin.autosend(
`Took ${time.toLocaleString()} ms\n\`\`\`js\n${result}\`\`\`\n`,
message);
} catch (e) {
return Util.Hastebin.autosend('```js\n' + e.stack + '\n```', message);
}
}
get metadata() { return {
category: 'Developer',
description: 'Evaluate code asynchronously.',
usage: '<code>',
note: 'Due to the added async IIFE wrapper in this command, ' +
'it is necessary to use the return statement to return a result.\n' +
'e.g. `return 1`'
}; }
};

View file

@ -1,35 +0,0 @@
/* jshint evil: true */
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class Eval extends Command {
get name() { return 'eval'; }
get _options() { return {
aliases: ['e'],
permissions: ['elevated'],
listed: false,
minimumArgs: 1
}; }
// eslint-disable-next-line no-unused-vars
async exec(message, opts) {
try {
const start = Date.now();
const result = eval(Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' '));
const time = Date.now() - start;
return Util.Hastebin.autosend(
`Took ${time.toLocaleString()} ms\n\`\`\`js\n${result}\`\`\`\n`,
message);
} catch (e) {
return Util.Hastebin.autosend('```js\n' + e.stack + '\n```', message);
}
}
get metadata() { return {
category: 'Developer',
description: 'Evaluate code.',
usage: '<code>'
}; }
};

View file

@ -1,34 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
const { exec } = require('child_process');
module.exports = class Exec extends Command {
get name() { return 'exec'; }
get _options() { return {
aliases: ['ex', 'sys'],
permissions: ['elevated'],
listed: false,
minimumArgs: 1
}; }
codeBlock(content, lang = null) {
return `\`\`\`${lang ? `${lang}\n` : ''}${content}\`\`\``;
}
async exec(message) {
await this.client.startTyping(message.channel);
exec(Util.Prefix.strip(message, this.client).split(' ').slice(1).join(' '), (err, stdout, stderr) => {
this.client.stopTyping(message.channel);
if (err) return message.channel.createMessage(this.codeBlock(err, 'js'));
const stdErrBlock = (stderr ? this.codeBlock(stderr, 'js') + '\n' : '');
return Util.Hastebin.autosend(stdErrBlock + this.codeBlock(stdout), message);
});
}
get metadata() { return {
category: 'Developer',
description: 'Do some terminal commands.',
usage: '<command> …'
}; }
};

View file

@ -1,23 +0,0 @@
const Command = require('../../structures/Command');
module.exports = class Reload extends Command {
get name() { return 'reload'; }
get _options() { return {
aliases: ['r'],
permissions: ['elevated'],
listed: false,
}; }
async exec(message) {
const sentMessage = await message.channel.createMessage('♻️ Reloading commands…');
this.client.cmds.reload();
this.client.cmds.preloadAll();
return sentMessage.edit('✅ Reloaded commands.');
}
get metadata() { return {
category: 'Developer',
description: 'Reloads all commands.'
}; }
};

View file

@ -1,46 +0,0 @@
const Command = require('../../structures/Command');
const fs = require('fs');
module.exports = class ReloadOne extends Command {
get name() { return 'reloadone'; }
get _options() { return {
aliases: ['r1', 'reloadsingle', 'rs'],
permissions: ['elevated'],
minimumArgs: 1,
listed: false,
}; }
async exec(message, { args }) {
const commands = args.map(name => this.client.cmds.get(name));
if (commands.includes(undefined))
return message.channel.createMessage('Invalid command!');
const fileExist = commands.map(command => {
const path = command.path;
const stat = fs.lstatSync(path);
return stat.isFile();
});
if (fileExist.includes(false))
return message.channel.createMessage('A file that had a specified command no longer exists!');
const sentMessage = await message.channel.createMessage('♻️ Reloading commands…');
const reloadedCommands = commands.map(command => {
const path = command.path;
const index = this.client.cmds.commands.indexOf(command);
this.client.cmds.commands.splice(index, 1);
const newCommand = this.client.cmds.load(path);
newCommand.preload();
return newCommand;
});
return sentMessage.edit(`✅ Reloaded ${reloadedCommands.map(c => `\`${c.name}\``).join(', ')}.`);
}
get metadata() { return {
category: 'Developer',
description: 'Reloads specific commands.',
usage: '<commandName> [commandName] …'
}; }
};

View file

@ -1,22 +0,0 @@
const Command = require('../../structures/Command');
module.exports = class Restart extends Command {
get name() { return 'restart'; }
get _options() { return {
aliases: ['re'],
permissions: ['elevated'],
listed: false,
}; }
async exec(message) {
await message.channel.createMessage('Restarting...');
await this.client.dieGracefully();
process.exit(0);
}
get metadata() { return {
category: 'Developer',
description: 'Restarts the bot.'
}; }
};

View file

@ -1,27 +0,0 @@
const Command = require('../structures/Command');
module.exports = class Ping extends Command {
get name() { return 'ping'; }
get _options() { return {
aliases: ['p', 'pong'],
cooldown: 0,
}; }
async exec(message) {
const currentPing = Array.from(this.client.shards.values())
.map(shard => shard.latency).reduce((prev, val) => prev + val, 0);
const timeBeforeMessage = Date.now();
const sentMessage = await message.channel.createMessage('> :ping_pong: ***Ping...***\n' +
`> WS: ${currentPing.toLocaleString()} ms`);
await sentMessage.edit(
'> :ping_pong: ***Pong!***\n' +
`> WS: ${currentPing.toLocaleString()} ms\n` +
`> REST: ${(Date.now() - timeBeforeMessage).toLocaleString()} ms`);
}
get metadata() { return {
category: 'General',
description: 'Pong!'
}; }
};

View file

@ -1,40 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class TAbandon extends Command {
get name() { return 'tabandon'; }
get _options() { return {
aliases: ['taban', 'tdrop'],
permissions: ['trustedOrAdmin'],
minimumArgs: 1
}; }
async exec(message, { args }) {
const givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
if (!await this.client.messageAwaiter.confirm(message, {
header:
'Are you sure you want to abandon a claim from a **trusted** account?'
})) return;
const account = await Util.LBRY.findSDKAccount(this.client, account => account.is_default);
// Drop support
const response = await this.client.lbry.abandonSupport({
accountID: account.id, claimID: givenClaim });
const transaction = await response.json();
if (await this.handleResponse(message, response, transaction)) return;
const txid = transaction.result.txid;
return message.channel.createMessage(`Abandon successful! https://explorer.lbry.com/tx/${txid}`);
}
get metadata() { return {
category: 'Trusted',
description: 'Abandons a support on a given claim from the trusted account.',
usage: '<claim>'
}; }
};

View file

@ -1,30 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
const config = require('config');
module.exports = class TBalance extends Command {
get name() { return 'tbalance'; }
get _options() { return {
aliases: ['tbal', 'trustedbal', 'trustedbalance'],
permissions: ['trustedOrAdmin']
}; }
async exec(message) {
const account = await Util.LBRY.findSDKAccount(this.client, account => account.is_default);
const response = await this.client.lbry.accountBalance(account.id);
const wallet = await response.json();
if (await this.handleResponse(message, response, wallet)) return;
return message.channel.createMessage({ embed: {
color: config.embedColor,
description: `**${wallet.result.available}** LBC is available in the trusted account.\n\n` +
`Reserved in Supports: ${wallet.result.reserved_subtotals.supports} LBC\n` +
`Total: ${wallet.result.total} LBC`
} });
}
get metadata() { return {
category: 'Trusted',
description: 'Shows the trusted wallet balance.'
}; }
};

View file

@ -1,51 +0,0 @@
const Command = require('../../structures/Command');
const Util = require('../../util');
module.exports = class TSupport extends Command {
get name() { return 'tsupport'; }
get _options() { return {
aliases: ['tsup'],
permissions: ['trustedOrAdmin'],
minimumArgs: 2
}; }
async exec(message, { args }) {
const givenAmount = Util.LBRY.ensureDecimal(args[1]);
if (!givenAmount)
return message.channel.createMessage('The second argument must be a numeric amount of LBC to send!');
const givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
// Get and check balance
const account = await Util.LBRY.findSDKAccount(this.client, account => account.is_default);
const walletResponse = await this.client.lbry.accountBalance(account.id);
const wallet = await walletResponse.json();
if (await this.handleResponse(message, walletResponse, wallet)) return;
const balance = wallet.result.available;
if (parseFloat(givenAmount) > parseFloat(balance))
return message.channel.createMessage('You don\'t have enough LBC to do this!');
if (!await this.client.messageAwaiter.confirm(message, {
header:
'Are you sure you want to support a claim from a **trusted** account?'
})) return;
// Create support
const response = await this.client.lbry.createSupport({
accountID: account.id, claimID: givenClaim, amount: givenAmount });
const transaction = await response.json();
if (await this.handleResponse(message, response, transaction)) return;
const txid = transaction.result.txid;
return message.channel.createMessage(`Support successful! https://explorer.lbry.com/tx/${txid}`);
}
get metadata() { return {
category: 'Trusted',
description: 'Support a given claim from the trusted account.',
usage: '<claim> <amount>'
}; }
};

View file

@ -1,42 +0,0 @@
const Command = require('../../structures/Command');
const GenericPager = require('../../structures/GenericPager');
const Util = require('../../util');
module.exports = class TSupports extends Command {
get name() { return 'tsupports'; }
get _options() { return {
aliases: ['tsups'],
permissions: ['trustedOrAdmin'],
minimumArgs: 0
}; }
async exec(message, { args }) {
let givenClaim;
if (args[0]) {
givenClaim = Util.resolveToClaimID(args[0]);
if (!givenClaim)
// @TODO use claim_search for invalid claim ids
return message.channel.createMessage('That Claim ID isn\'t valid.');
}
const account = await Util.LBRY.findSDKAccount(this.client, account => account.is_default);
const supportsCount = await Util.LBRY.getSupportsCount(this.client, account.id);
if (supportsCount <= 0)
return message.channel.createMessage('No supports found.');
const supportsResponse = await this.client.lbry.listSupports({
accountID: account.id, page_size: supportsCount, claimID: givenClaim });
const supports = (await supportsResponse.json()).result.items;
const paginator = new GenericPager(this.client, message, {
items: supports,
header: `All supports for the trusted account${
givenClaim ? ` on claim \`${givenClaim}\`` : ''}`, itemTitle: 'Supports', itemsPerPage: 5,
display: item => `> ${item.name} \`${item.claim_id}\`\n> ${item.amount} LBC\n`
});
return paginator.start(message.channel.id, message.author.id);
}
get metadata() { return {
category: 'Trusted',
description: 'Shows the list of supports from the trusted account.',
usage: '[claimID]'
}; }
};

View file

@ -0,0 +1,10 @@
import { DexareClient, MemoryDataManager } from 'dexare';
import { CurateConfig } from '../bot';
export const name = 'flush-cache';
export const time = '0 * * * *';
export const start = true;
export async function onTick(client: DexareClient<CurateConfig>) {
const data = client.data as MemoryDataManager;
data.flushThrottles();
}

View file

@ -1,139 +0,0 @@
const redis = require('redis');
const { EventEmitter } = require('eventemitter3');
/**
* The Redis database handler
*/
module.exports = class Database extends EventEmitter {
constructor(client) {
super();
this.client = client;
this.reconnectAfterClose = true;
console.init('Redis initialized');
}
/**
* Creates a client and connects to the database
* @param {Object} options
*/
connect({ host = 'localhost', port, password, prefix }) {
console.info('Connecting to redis...');
return new Promise((resolve, reject) => {
this.redis = redis.createClient({ host, port, password, prefix });
this.redis.on('error', this.onError.bind(this));
this.redis.on('warning', w => console.warn('Redis Warning', w));
this.redis.on('end', () => this.onClose.bind(this));
this.redis.on('reconnecting', () => console.warn('Reconnecting to redis...'));
this.redis.on('ready', () => console.info('Redis client ready.'));
this.redis.on('connect', () => console.info('Redis connection has started.'));
this.host = host;
this.port = port;
this.password = password;
this.redis.once('ready', resolve.bind(this));
this.redis.once('error', reject.bind(this));
});
}
// #region Redis functions
hget(key, hashkey) {
return new Promise((resolve, reject) => {
this.redis.HGET(key, hashkey, (err, value) => {
if (err) reject(err);
resolve(value);
});
});
}
hset(key, hashkey, value) {
return new Promise((resolve, reject) => {
this.redis.HSET(key, hashkey, value, (err, res) => {
if (err) reject(err);
resolve(res);
});
});
}
incr(key) {
return new Promise((resolve, reject) => {
this.redis.incr(key, (err, res) => {
if (err) reject(err);
resolve(res);
});
});
}
get(key) {
return new Promise((resolve, reject) => {
this.redis.get(key, function(err, reply) {
if (err) reject(err);
resolve(reply);
});
});
}
expire(key, ttl) {
return new Promise((resolve, reject) => {
this.redis.expire(key, ttl, (err, value) => {
if (err) reject(err);
resolve(value);
});
});
}
exists(key) {
return new Promise((resolve, reject) => {
this.redis.exists(key, (err, value) => {
if (err) reject(err);
resolve(value === 1);
});
});
}
set(key, value) {
return new Promise((resolve, reject) => {
this.redis.set(key, value, (err, res) => {
if (err) reject(err);
resolve(res);
});
});
}
// #endregion
/**
* Reconnects the client
*/
async reconnect() {
console.warn('Attempting redis reconnection');
this.conn = await this.connect(this);
}
/**
* Disconnects the client
*/
disconnect() {
this.reconnectAfterClose = false;
return new Promise(resolve => {
this.redis.once('end', resolve);
this.redis.quit();
});
}
/**
* @private
*/
onError(err) {
console.error('Redis Error', err);
this.emit('error', err);
}
/**
* @private
*/
async onClose() {
console.error('Redis closed');
this.emit('close');
if (this.reconnectAfterClose) await this.reconnect();
}
};

View file

@ -1,50 +0,0 @@
const ArgumentInterpreter = require('./structures/ArgumentInterpreter');
const Util = require('./util');
const config = require('config');
module.exports = class Events {
constructor(client) {
this.client = client;
client.on('messageCreate', this.onMessage.bind(this));
client.on('messageReactionAdd', this.onReaction.bind(this));
}
async onMessage(message) {
if (message.author.bot || message.author.system) return;
// Check to see if bot can send messages
if (message.channel.type !== 1 &&
!message.channel.permissionsOf(this.client.user.id).has('sendMessages')) return;
// Message awaiter
if (this.client.messageAwaiter.processHalt(message)) return;
// Command parsing
const argInterpretor = new ArgumentInterpreter(Util.Prefix.strip(message, this.client));
const args = argInterpretor.parseAsStrings();
const commandName = args.splice(0, 1)[0];
const command = this.client.cmds.get(commandName, message);
if (!message.content.match(Util.Prefix.regex(this.client)) || !command) return;
try {
await command._exec(message, {
args
});
} catch (e) {
if (config.debug) {
console.error(`The '${command.name}' command failed.`);
console.log(e);
}
message.channel.createMessage(':fire: An error occurred while processing that command!');
this.client.stopTyping(message.channel);
}
}
onReaction(message, emoji, member) {
const id = `${message.id}:${member.id}`;
if (this.client.messageAwaiter.reactionCollectors.has(id)) {
const collector = this.client.messageAwaiter.reactionCollectors.get(id);
collector._onReaction(emoji, member.id);
}
}
};

9
src/index.ts Normal file
View file

@ -0,0 +1,9 @@
import path from 'path';
// Config fix for running in devscript
if (path.parse(process.cwd()).name === 'dist')
process.env.NODE_CONFIG_DIR = path.join(process.cwd(), '..', 'config');
import { connect } from './bot';
connect();

View file

@ -1,154 +0,0 @@
const Halt = require('./structures/Halt');
const ReactionCollector = require('./structures/ReactionCollector');
/**
* Handles async message functions
*/
class MessageAwaiter {
constructor(client) {
this.client = client;
this.halts = new Map();
this.reactionCollectors = new Map();
}
/**
* Creates a halt. This pauses any events in the event handler for a specific channel and user.
* This allows any async functions to handle any follow-up messages.
* @param {string} channelID The channel's ID
* @param {string} userID The user's ID
* @param {number} [timeout=30000] The time until the halt is auto-cleared
*/
createHalt(channelID, userID, timeout = 30000) {
const id = `${channelID}:${userID}`;
if (this.halts.has(id)) this.halts.get(id).end();
const halt = new Halt(this, timeout);
halt.once('end', () => this.halts.delete(id));
this.halts.set(id, halt);
return halt;
}
/**
* Creates a reaction collector. Any reactions from the user will be emitted from the collector.
* @param {string} message The message to collect from
* @param {string} userID The user's ID
* @param {number} [timeout=30000] The time until the halt is auto-cleared
*/
createReactionCollector(message, userID, timeout = 30000) {
const id = `${message.id}:${userID}`;
if (this.reactionCollectors.has(id)) this.reactionCollectors.get(id).end();
const collector = new ReactionCollector(this, timeout);
collector.once('end', () => this.reactionCollectors.delete(id));
this.reactionCollectors.set(id, collector);
return collector;
}
/**
* Gets an ongoing halt based on a message
* @param {Message} message
*/
getHalt(message) {
const id = `${message.channel.id}:${message.author.id}`;
return this.halts.get(id);
}
/**
* Processes a halt based on a message
* @param {Message} message
*/
processHalt(message) {
const id = `${message.channel.id}:${message.author.id}`;
if (this.halts.has(id)) {
const halt = this.halts.get(id);
halt._onMessage(message);
return true;
}
return false;
}
/**
* Awaits the next message from a user
* @param {Message} message The message to wait for
* @param {Object} [options] The options for the await
* @param {number} [options.filter] The message filter
* @param {number} [options.timeout=30000] The timeout for the halt
* @returns {?Message}
*/
awaitMessage(message, { filter = () => true, timeout = 30000 } = {}) {
return new Promise(resolve => {
const halt = this.createHalt(message.channel.id, message.author.id, timeout);
let foundMessage = null;
halt.on('message', nextMessage => {
if (filter(nextMessage)) {
foundMessage = nextMessage;
halt.end();
}
});
halt.on('end', () => resolve(foundMessage));
});
}
/**
* Same as {@see #awaitMessage}, but is used for getting user input via next message
* @param {Message} message The message to wait for
* @param {Object} [options] The options for the await
* @param {number} [options.filter] The message filter
* @param {number} [options.timeout=30000] The timeout for the halt
* @param {string} [options.header] The content to put in the bot message
* @returns {?Message}
*/
async getInput(message, { filter = () => true, timeout = 30000, header = null } = {}) {
await message.channel.createMessage(`<@${message.author.id}>, ` +
(header || 'Type the message you want to input.') + '\n\n' +
`Typing "<@!${this.client.user.id}> cancel" will cancel the input.`);
return new Promise(resolve => {
const halt = this.createHalt(message.channel.id, message.author.id, timeout);
let handled = false, input = null;
halt.on('message', nextMessage => {
if (filter(nextMessage)) {
const cancelRegex = new RegExp(`^(?:<@!?${this.client.user.id}>\\s?)(cancel|stop|end)$`);
if (!nextMessage.content || cancelRegex.test(nextMessage.content.toLowerCase())) {
handled = true;
message.channel.createMessage(`<@${message.author.id}>, Your last input was canceled.`);
} else input = nextMessage.content;
halt.end();
}
});
halt.on('end', async () => {
if (!input && !handled)
await message.channel.createMessage(`<@${message.author.id}>, Your last input was canceled.`);
resolve(input);
});
});
}
/**
* Same as {@see #awaitMessage}, but is used for confirmation
* @param {Message} message The message to wait for
* @param {Object} [options] The options for the await
* @param {number} [options.timeout=30000] The timeout for the halt
* @param {string} [options.header] The content to put in the bot message
* @returns {?Message}
*/
async confirm(message, { timeout = 30000, header = null } = {}) {
await message.channel.createMessage(`<@${message.author.id}>, ` +
(header || 'Are you sure you want to do this?') + '\n\n' +
'Type `yes` to confirm. Any other message will cancel the confirmation.');
return new Promise(resolve => {
const halt = this.createHalt(message.channel.id, message.author.id, timeout);
let input = false;
halt.on('message', nextMessage => {
input = nextMessage.content === 'yes';
halt.end();
});
halt.on('end', async () => {
if (!input)
await message.channel.createMessage(`<@${message.author.id}>, Confirmation canceled.`);
resolve(input);
});
});
}
}
MessageAwaiter.Halt = Halt;
module.exports = MessageAwaiter;

View file

@ -1,63 +0,0 @@
const Sequelize = require('sequelize');
module.exports = class SQLiteDB {
constructor() {
this.sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'config/curate.sqlite',
define: { timestamps: false }
});
class UserPair extends Sequelize.Model {}
UserPair.init({
discordID: {
type: Sequelize.STRING,
allowNull: false,
primaryKey: true,
unique: true,
},
lbryID: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
}
}, { sequelize: this.sequelize, modelName: 'user' });
UserPair.sync();
this.model = UserPair;
}
/**
* Gets a pair from a Discord ID
* @param {string} id
*/
async get(id) {
const item = await this.model.findOne({ where: { discordID: id } });
return item ? item.get({ plain: true }) : null;
}
/**
* Creates an ID pair
* @param {string} discordID
* @param {string} lbryID
*/
pair(discordID, lbryID) {
return this.model.create({ discordID, lbryID });
}
/**
* Removes an ID pair
* @param {string} discordID
*/
remove(discordID) {
return this.model.destroy({ where: { discordID } });
}
/**
* Gets all pairs in the database
*/
async getAll() {
const items = await this.model.findAll();
return items.map(item => item.get({ plain: true }));
}
};

View file

@ -1,182 +0,0 @@
/**
* A class that iterates a string's index
* @see ArgumentInterpreter
*/
class StringIterator {
/**
* @param {string} string The string to iterate through
*/
constructor(string) {
this.string = string;
this.index = 0;
this.previous = 0;
this.end = string.length;
}
/**
* Get the character on an index and moves the index forward.
* @returns {?string}
*/
get() {
const nextChar = this.string[this.index];
if (!nextChar)
return nextChar;
else {
this.previous += this.index;
this.index += 1;
return nextChar;
}
}
/**
* Reverts to the previous index.
*/
undo() {
this.index = this.previous;
}
/**
* The previous character that was used
* @type {string}
*/
get prevChar() {
return this.string[this.previous];
}
/**
* Whether or not the index is out of range
* @type {boolean}
*/
get inEOF() {
return this.index >= this.end;
}
}
/**
* Parses arguments from a message.
*/
class ArgumentInterpreter {
/**
* @param {string} string The string that will be parsed for arguments
* @param {?Object} options The options for the interpreter
* @param {?boolean} [options.allowWhitespace=false] Whether to allow whitespace characters in the arguments
*/
constructor(string, { allowWhitespace = false } = {}) {
this.string = string;
this.allowWhitespace = allowWhitespace;
}
/**
* Parses the arguements as strings.
* @returns {Array<String>}
*/
parseAsStrings() {
const args = [];
let currentWord = '';
let quotedWord = '';
const string = this.allowWhitespace ? this.string : this.string.trim();
const iterator = new StringIterator(string);
while (!iterator.inEOF) {
const char = iterator.get();
if (char === undefined) break;
if (this.isOpeningQuote(char) && iterator.prevChar !== '\\') {
currentWord += char;
const closingQuote = ArgumentInterpreter.QUOTES[char];
// Quote iteration
while (!iterator.inEOF) {
const quotedChar = iterator.get();
// Unexpected EOF
if (quotedChar === undefined) {
args.push(...currentWord.split(' '));
break;
}
if (quotedChar == '\\') {
currentWord += quotedChar;
const nextChar = iterator.get();
if (nextChar === undefined) {
args.push(...currentWord.split(' '));
break;
}
currentWord += nextChar;
// Escaped quote
if (ArgumentInterpreter.ALL_QUOTES.includes(nextChar)) {
quotedWord += nextChar;
} else {
// Ignore escape
quotedWord += quotedChar + nextChar;
}
continue;
}
// Closing quote
if (quotedChar == closingQuote) {
currentWord = '';
args.push(quotedWord);
quotedWord = '';
break;
}
currentWord += quotedChar;
quotedWord += quotedChar;
}
continue;
}
if (/^\s$/.test(char)) {
if (currentWord)
args.push(currentWord);
currentWord = '';
continue;
}
currentWord += char;
}
if (currentWord.length)
args.push(...currentWord.split(' '));
return args;
}
/**
* Checks whether or not a character is an opening quote
* @param {string} char The character to check
*/
isOpeningQuote(char) {
return Object.keys(ArgumentInterpreter.QUOTES).includes(char);
}
}
// Opening / Closing
ArgumentInterpreter.QUOTES = {
'"': '"',
'': '',
'': '',
'“': '”',
'„': '‟',
'⹂': '⹂',
'「': '」',
'『': '』',
'〝': '〞',
'﹁': '﹂',
'﹃': '﹄',
'': '',
'「': '」',
'«': '»',
'': '',
'《': '》',
'〈': '〉',
};
ArgumentInterpreter.ALL_QUOTES = Object.keys(ArgumentInterpreter.QUOTES)
.map(i => ArgumentInterpreter.QUOTES[i])
.concat(Object.keys(ArgumentInterpreter.QUOTES));
ArgumentInterpreter.StringIterator = StringIterator;
module.exports = ArgumentInterpreter;

View file

@ -1,149 +0,0 @@
const Util = require('../util');
const config = require('config');
/**
* A command in the bot.
*/
class Command {
/**
* @param {TrelloBot} client
*/
constructor(client) {
this.client = client;
this.subCommands = {};
}
/**
* @private
*/
_preload() {
if (!this.preload() && config.debug)
this.client.cmds.logger.info('Preloading command', this.name);
}
/**
* The function executed while loading the command into the command handler.
*/
preload() {
return true;
}
/**
* @private
* @param {Message} message
* @param {Object} opts
*/
async _exec(message, opts) {
// Check minimum arguments
if (this.options.minimumArgs > 0 && opts.args.length < this.options.minimumArgs)
return message.channel.createMessage(
`${this.options.minimumArgsMessage}\nUsage: ${config.prefix}${this.name}${
this.metadata.usage ?
` \`${this.metadata.usage}\`` : ''}`);
// Check commmand permissions
const curators = Array.isArray(config.curatorRoleID) ? config.curatorRoleID : [config.curatorRoleID];
const admins = Array.isArray(config.adminRoleID) ? config.adminRoleID : [config.adminRoleID];
const trusteds = Array.isArray(config.trustedRoleID) ? config.trustedRoleID : [config.trustedRoleID];
if (this.options.permissions.length)
for (const i in this.options.permissions) {
const perm = this.options.permissions[i];
if (!Util.CommandPermissions[perm])
throw new Error(`Invalid command permission "${perm}"`);
if (!Util.CommandPermissions[perm](this.client, message, opts))
return message.channel.createMessage({
attach: 'I need the permission `Attach Files` to use this command!',
embed: 'I need the permission `Embed Links` to use this command!',
emoji: 'I need the permission `Use External Emojis` to use this command!',
elevated: 'Only the elevated users of the bot can use this command!',
curator: `This command requires you to have the ${
curators.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} role!`,
admin: `This command requires you to have the ${
admins.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} role!`,
curatorOrAdmin: `This command requires you to have the ${
curators.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} or ${
admins.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} role!`,
trustedOrAdmin: `This command requires you to have the ${
trusteds.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} or ${
admins.map(id =>
`"${this.client.guilds.get(config.guildID).roles.get(id).name}"`).join('/')} role!`,
guild: 'This command must be ran in a guild!',
}[perm]);
}
// Process cooldown
if (!this.cooldownAbs || await this.client.cmds.processCooldown(message, this)) {
await this.exec(message, opts);
} else {
const cd = await this.client.db.hget(`cooldowns:${message.author.id}`, this.name);
return message.channel.createMessage(
`:watch: This command is on cooldown! Wait ${
Math.ceil(this.cooldownAbs - (Date.now() - cd))} second(s) before doing this again!`);
}
}
// eslint-disable-next-line no-empty-function, no-unused-vars
exec(Message, opts) { }
/**
* @private
*/
async handleResponse(message, response, json) {
if (!json) json = await response.json();
if (response.status !== 200 || json.error) {
const error = response.status === 500 ?
{ message: 'Internal server error' } : json.error;
console.error(`SDK error in ${this.name}:${message.author.id}`, response, error);
await message.channel.createMessage(
`LBRY-SDK returned ${response.status} with an error: \`${error.message}\``);
return true;
}
return false;
}
/**
* The options for the command
* @type {Object}
*/
get options() {
const options = {
aliases: [],
cooldown: 2,
listed: true,
minimumArgs: 0,
permissions: [],
minimumArgsMessage: 'Not enough arguments!',
};
Object.assign(options, this._options);
return options;
}
/**
* @private
*/
_options() { return {}; }
/**
* The cooldown in milliseconnds
* @returns {number}
*/
get cooldownAbs() { return this.options.cooldown * 1000; }
/**
* The metadata for the command
* @return {Object}
*/
get metadata() {
return {
category: 'Misc.',
};
}
}
module.exports = Command;

View file

@ -1,103 +0,0 @@
const Paginator = require('./Paginator');
const lodash = require('lodash');
const config = require('config');
/**
* A generic pager that shows a list of items
*/
class GenericPager extends Paginator {
/**
* @param {TrelloBot} client The client to use
* @param {Message} message The user's message to read permissions from
* @param {Object} options The options for the pager
* @param {Array} options.items The items the paginator will display
* @param {number} [options.itemsPerPage=15] How many items a page will have
* @param {Function} [options.display] The function that will be used to display items on the prompt
* @param {Object} [options.embedExtra] The embed object to add any extra embed elements to the prompt
* @param {string} [options.itemTitle='words.item.many'] The title to use for the items
* @param {string} [options.header] The text to show above the prompt
* @param {string} [options.footer] The text to show below the prompt
*/
constructor(client, message, {
items = [], itemsPerPage = 15,
display = item => item.toString(),
embedExtra = {}, itemTitle = 'Items',
header = null, footer = null
} = {}) {
super(client, message, { items, itemsPerPage });
this.displayFunc = display;
this.embedExtra = embedExtra;
this.itemTitle = itemTitle;
this.header = header;
this.footer = footer;
}
/**
* Whether or not this instance can use embed
* @returns {boolean}
*/
canEmbed() {
return this.message.channel.type === 1 ||
this.message.channel.permissionsOf(this.client.user.id).has('embedLinks');
}
/**
* Updates the current message
* @returns {Promise}
*/
updateMessage() {
return this.message.edit(this.currentMessage).catch(() => {});
}
/**
* The message for the current page
* @type {Object|string}
*/
get currentMessage() {
const displayPage = this.page.map((item, index) =>
this.displayFunc(item, index, ((this.pageNumber - 1) * this.itemsPerPage) + index));
if (this.canEmbed()) {
const embed = lodash.defaultsDeep({
title: `${this.itemTitle} ` +
`(${this.items.length}, Page ${this.pageNumber}/${this.maxPages})`,
description: this.header || undefined,
footer: this.footer ? { text: this.footer } : undefined,
fields: []
}, this.embedExtra, { color: config.embedColor });
embed.fields.push({
name: '*List Prompt*',
value: displayPage.join('\n')
});
return { embed };
} else {
const top = `${this.itemTitle} ` +
`(${this.items.length}, Page ${this.pageNumber}/${this.maxPages})`;
const lines = '─'.repeat(top.length);
return (this.header || '') + '```prolog\n' + `${top}\n` + `${lines}\n` +
displayPage.join('\n') + `${lines}\`\`\`` + (this.footer || '');
}
}
/**
* Starts the reaction collector and pagination
* @param {string} channelID The channel to post the new message to
* @param {string} userID The user's ID that started the process
* @param {number} timeout
*/
async start(channelID, userID, timeout) {
this.message = await this.client.createMessage(channelID, this.currentMessage);
return super.start(userID, timeout);
}
/**
* @private
*/
_change() {
this.updateMessage().catch(() => this.collector.end());
this.emit('change', this.pageNumber);
}
}
module.exports = GenericPager;

View file

@ -1,141 +0,0 @@
const GenericPager = require('./GenericPager');
const Paginator = require('./Paginator');
const lodash = require('lodash');
const fuzzy = require('fuzzy');
/**
* A generic pager that shows a list of items
*/
class GenericPrompt {
/**
* @param {TrelloBot} client The client to use
* @param {Message} message The user's message to read permissions from
* @param {Object} pagerOptions The options for the pager
*/
constructor(client, message, pagerOptions = {}) {
this.client = client;
this.message = message;
this.pagerOptions = pagerOptions;
this.displayFunc = pagerOptions.display || ((item) => item.toString());
// Override some pager options
this.pagerOptions.display = (item, i, ai) => `${ai + 1}. ${this.displayFunc(item, i, ai)}`;
this.pagerOptions.header = pagerOptions.header || 'Type the number of the item you want to use.';
this.pagerOptions.footer = (pagerOptions.footer ? pagerOptions.footer + '\n\n' : '') +
'Typing "cancel" will close this prompt.';
this.pagerOptions.embedExtra = this.pagerOptions.embedExtra || {};
this.pagerOptions.embedExtra.author = {
name: `${message.author.username}#${message.author.discriminator}`,
icon_url: message.author.avatarURL || message.author.defaultAvatarURL
};
this.pager = new GenericPager(client, message, this.pagerOptions);
this.halt = null;
}
/**
* Starts the prompt
* @param {string} channelID The channel to post the new message to
* @param {string} userID The user's ID that started the process
* @param {number} timeout
*/
async choose(channelID, userID, timeout) {
if (this.pager.items.length === 0)
return null;
else if (this.pager.items.length === 1)
return this.pager.items[0];
await this.pager.start(channelID, userID, timeout);
this.halt = this.client.messageAwaiter.createHalt(channelID, userID, timeout);
// Sync timeouts
if (this.pager.collector)
this.pager.collector.restart();
this.halt.restart();
return new Promise(resolve => {
let foundItem = null;
this.halt.on('message', nextMessage => {
if (this.pager.canManage())
nextMessage.delete().catch(() => {});
if (GenericPrompt.CANCEL_TRIGGERS.includes(nextMessage.content.toLowerCase())) {
foundItem = { _canceled: true };
this.halt.end();
}
const chosenIndex = parseInt(nextMessage.content);
if (chosenIndex <= 0) return;
const chosenItem = this.pager.items[chosenIndex - 1];
if (chosenItem !== undefined) {
foundItem = chosenItem;
this.halt.end();
}
});
this.halt.on('end', () => {
// In case the halt ends before reactions are finished coming up
this.pager.reactionsCleared = true;
if (this.pager.collector)
this.pager.collector.end();
this.pager.message.delete().catch(() => {});
if (foundItem && foundItem._canceled)
foundItem = null;
else if (foundItem === null)
this.pager.message.channel.createMessage(
`<@${userID}>, Your last prompt was timed out.`).catch(() => {});
resolve(foundItem);
});
if (this.pager.collector)
this.pager.collector.on('reaction', emoji => {
if (Paginator.STOP === emoji.name) {
foundItem = { _canceled: true };
this.halt.end();
}
});
});
}
/**
* Filters the items into a search and prompts results.
* @param {string} query The term to search for
* @param {Object} options The options passed on to {@see #choose} .
* @param {string} options.channelID The channel to post the new message to
* @param {string} options.userID The user's ID that started the process
* @param {number} options.timeout
* @param {string|Function} [key='name'] The path to use for searches
*/
async search(query, { channelID, userID, timeout }, key = 'name') {
if (!query)
return this.choose(channelID, userID, timeout);
const results = fuzzy.filter(query, this.pager.items, {
extract: item => {
if (typeof key === 'string')
return lodash.get(item, key);
else if (typeof key === 'function')
return key(item);
else if (key === null)
return item;
}
}).map(el => el.original);
if (!results.length)
return { _noresults: true };
const tempItems = this.pager.items;
this.pager.items = results;
const result = await this.choose(channelID, userID, timeout);
this.pager.items = tempItems;
return result;
}
}
GenericPrompt.CANCEL_TRIGGERS = [
'c', 'cancel', 's', 'stop'
];
module.exports = GenericPrompt;

View file

@ -1,61 +0,0 @@
const EventEmitter = require('eventemitter3');
/**
* A class that represents a message halt
*/
class Halt extends EventEmitter {
constructor(messageAwaiter, timeout) {
super();
this.messageAwaiter = messageAwaiter;
this.timeout = timeout;
this.interval = null;
this.ended = false;
this.messages = new Map();
this._endBind = this._end.bind(this);
this._start();
}
/**
* Restarts the halt.
* @param {number} [timeout] The new timeout to halt by
*/
restart(timeout) {
if (this.ended) return;
clearTimeout(this.interval);
this.interval = setTimeout(this._endBind, timeout || this.timeout);
}
/**
* Ends the halt.
*/
end() {
if (this.ended) return;
clearTimeout(this.interval);
this._end();
}
/**
* @private
*/
_onMessage(message) {
this.messages.set(message.id, message);
this.emit('message', message);
}
/**
* @private
*/
_start() {
this.interval = setTimeout(this._endBind, this.timeout);
}
/**
* @private
*/
_end() {
this.ended = true;
this.emit('end');
}
}
module.exports = Halt;

View file

@ -1,210 +0,0 @@
const fetch = require('node-fetch');
const AbortController = require('abort-controller');
const config = require('config');
const Util = require('../util');
class LBRY {
constructor(client) {
this.client = client;
}
_request(options = {}) {
if (!options.url)
throw new Error('No URL was provided!');
if (!options.method)
options.method = 'get';
const url = new URL(options.noBase ? options.url : config.sdkURL + options.url);
let body = options.body;
// Query params
if (options.query && Object.keys(options.query).length)
Object.keys(options.query).map(key =>
url.searchParams.append(key, options.query[key]));
// Body Format
if (body && options.bodyType === 'json')
body = JSON.stringify(body);
else if (body && options.bodyType === 'form') {
body = new URLSearchParams();
Object.keys(options.body).forEach(key =>
body.append(key, options.body[key]));
}
// Hash
if (options.hash)
url.hash = options.hash;
// User Agent
const userAgent = `LBRYCurate (https://github.com/LBRYFoundation/curate ${this.client.pkg.version}) Node.js/${process.version}`;
if (!options.headers)
options.headers = {
'User-Agent': userAgent
};
else
options.headers['User-Agent'] = userAgent;
// Abort Controller
const controller = new AbortController();
const controllerTimeout = setTimeout(controller.abort.bind(controller), 5000);
return new Promise((resolve, reject) => {
fetch(url.href, {
body,
headers: options.headers,
method: options.method,
signal: controller.signal
}).then(r => {
clearTimeout(controllerTimeout);
resolve(r);
}).catch(e => {
clearTimeout(controllerTimeout);
if (e && e.type === 'aborted')
resolve(e); else reject(e);
});
});
}
_sdkRequest(method, params = {}) {
const payload = { method };
if (params && Object.keys(params).length)
payload.params = params;
return this._request({
url: '/',
method: 'post',
bodyType: 'json',
body: payload
});
}
// #region Account Methods
/**
* List details of all of the accounts or a specific account.
*/
listAccounts(params) {
return this._sdkRequest('account_list', params);
}
/**
* Create a new account.
* @param {string} accountName The account's name
*/
createAccount(accountName) {
return this._sdkRequest('account_create', { account_name: accountName, single_key: true });
}
/**
* Return the balance of an account
* @param {string} accountID The account's ID
*/
accountBalance(accountID) {
return this._sdkRequest('account_balance', { account_id: accountID });
}
/**
* Transfer some amount (or --everything) to an account from another account (can be the same account).
* @param {object} options
* @param {string} options.to The account ID to fund
* @param {string} options.from The account ID to fund from
* @param {boolean} options.everything Transfer everything
* @param {string} options.amount The amount to fund (integer/float string)
*/
fundAccount({ to, from, everything, amount }) {
return this._sdkRequest('account_fund', {
to_account: to, from_account: from, everything,
amount: Util.LBRY.ensureDecimal(amount), broadcast: true });
}
/**
* Remove an existing account.
* @param {string} accountID The account's ID
*/
removeAccount(accountID) {
return this._sdkRequest('account_remove', { account_id: accountID });
}
// #endregion
// #region Support Methods
/**
* List supports and tips in my control.
* @param {object} options
* @param {string} options.accountID The account ID to list
* @param {string|Array<string>} options.claimID The clain ID to list
*/
listSupports({ accountID, claimID }) {
return this._sdkRequest('support_list', { account_id: accountID, claim_id: claimID });
}
/**
* Create a support or a tip for name claim.
* @param {object} options
* @param {string} options.accountID The account ID to use
* @param {string} options.claimID The claim ID to use
* @param {number} options.amount The amount of support
*/
createSupport({ accountID, claimID, amount }) {
return this._sdkRequest('support_create', {
account_id: accountID, claim_id: claimID,
amount: Util.LBRY.ensureDecimal(amount), funding_account_ids: [accountID] });
}
/**
* Abandon supports, including tips, of a specific claim, optionally keeping some amount as supports.
* @param {object} options
* @param {string} options.accountID The account ID to use
* @param {string} options.claimID The claim ID to use
*/
abandonSupport({ accountID, claimID }) {
return this._sdkRequest('support_abandon', { account_id: accountID, claim_id: claimID });
}
// #endregion
// #region Wallet, Address, Claim Methods
/**
* Return the balance of a wallet
*/
walletBalance() {
return this._sdkRequest('wallet_balance');
}
/**
* Send the same number of credits to multiple
* addresses using all accounts in wallet to fund the
* transaction and the default account to receive any change.
* @param {object} options
* @param {string} options.to The wallet address to fund
* @param {string} options.amount The amount to send
*/
sendToWallet({ amount, to }) {
return this._sdkRequest('wallet_send', { amount: Util.LBRY.ensureDecimal(amount), addresses: to });
}
/**
* List account addresses or details.
* @param {object} options
* @param {string} options.to How many items should be per page
* @param {string} options.amount The amount to send
*/
listAddresses({ page_size = 1, account_id } = {}) {
return this._sdkRequest('address_list', { page_size, account_id });
}
/**
* Search for stream and channel claims on the blockchain.
* @param {object} options
* @param {string} options.name The claim name to search
* @param {string} options.text The text to search
* @param {string} options.claimID The claim ID to search
* @param {string} options.channel Signed channel name (e.g: @Coolguy3289)
* @param {string} options.channelType The type of claim
*/
searchClaim({ name, text, claimID, channel, channelType }) {
return this._sdkRequest('claim_search', {
name, text, claim_id: claimID, channel, channel_type: channelType });
}
// #endregion
}
module.exports = LBRY;

View file

@ -1,145 +0,0 @@
const EventEmitter = require('eventemitter3');
const GenericPager = require('./GenericPager');
const Paginator = require('./Paginator');
const lodash = require('lodash');
/**
* A prompt that allows users to toggle multiple values
*/
class MultiSelect extends EventEmitter {
/**
* @param {TrelloBot} client The client to use
* @param {Message} message The user's message to read permissions from
* @param {Object} options The options for the multi-select
* @param {string|Array<string>} options.path The path of the boolean
* @param {string} [options.checkEmoji] The emoji that resembles true
* @param {string} [options.uncheckEmoji] The emoji that resembles false
* @param {Object} pagerOptions The options for the pager
*/
constructor(client, message, { path, checkEmoji = '☑️', uncheckEmoji = '⬜' }, pagerOptions = {}) {
super();
this.client = client;
this.message = message;
this.pagerOptions = pagerOptions;
this.displayFunc = pagerOptions.display || ((item) => item.toString());
this.boolPath = path;
// Override some pager options
this.pagerOptions.display = (item, i, ai) => {
const value = lodash.get(item, this.boolPath);
return `\`[${ai + 1}]\` ${value ? checkEmoji : uncheckEmoji} ${this.displayFunc(item, i, ai)}`;
};
this.pagerOptions.header = pagerOptions.header || 'Type the number of the item to toggle its value.';
this.pagerOptions.footer = (pagerOptions.footer ? pagerOptions.footer + '\n\n' : '') +
'Type "save" to save the selection, otherwise type "cancel" to exit.';
this.pagerOptions.embedExtra = this.pagerOptions.embedExtra || {};
this.pagerOptions.embedExtra.author = {
name: `${message.author.username}#${message.author.discriminator}`,
icon_url: message.author.avatarURL || message.author.defaultAvatarURL
};
this.pager = new GenericPager(client, message, this.pagerOptions);
this.halt = null;
}
/**
* Starts the prompt
* @param {string} channelID The channel to post the new message to
* @param {string} userID The user's ID that started the process
* @param {number} timeout
*/
async start(channelID, userID, timeout) {
if (this.pager.items.length === 0)
return null;
await this.pager.start(channelID, userID, timeout);
// React with done
if (this.pager.collector)
await this.pager.message.addReaction(MultiSelect.DONE);
this.halt = this.client.messageAwaiter.createHalt(channelID, userID, timeout);
// Sync timeouts
if (this.pager.collector)
this.pager.collector.restart();
this.halt.restart();
return new Promise(resolve => {
let result = null;
this.halt.on('message', nextMessage => {
if (this.pager.canManage())
nextMessage.delete();
if (MultiSelect.CANCEL_TRIGGERS.includes(nextMessage.content.toLowerCase())) {
result = { _canceled: true };
this.halt.end();
} else if (MultiSelect.DONE_TRIGGERS.includes(nextMessage.content.toLowerCase())) {
result = this.pager.items;
this.halt.end();
}
// Find and update item
const chosenIndex = parseInt(nextMessage.content);
if (chosenIndex <= 0) return;
let chosenItem = this.pager.items[chosenIndex - 1];
if (chosenItem !== undefined) {
const oldItem = chosenItem;
chosenItem = lodash.set(chosenItem, this.boolPath, !lodash.get(chosenItem, this.boolPath));
this.emit('update', oldItem, chosenItem, chosenIndex - 1);
this.pager.items.splice(chosenIndex - 1, 1, chosenItem);
this.pager.updateMessage();
}
this.halt.restart();
if (this.pager.collector)
this.pager.collector.restart();
});
this.halt.on('end', () => {
// In case the halt ends before reactions are finished coming up
this.pager.reactionsCleared = true;
if (this.pager.collector)
this.pager.collector.end();
this.pager.message.delete().catch(() => {});
if (result && result._canceled) {
this.pager.message.channel.createMessage(
`<@${userID}>, Your last prompt was canceled. All changes have been lost.`);
result = null;
} else if (result === null)
this.pager.message.channel.createMessage(
`<@${userID}>, Your last prompt was timed out. All changes have been lost.`);
resolve(result);
});
if (this.pager.collector) {
this.pager.collector.on('clearReactions', () => {
if (!this.pager.canManage())
this.pager.message.removeReaction(MultiSelect.DONE).catch(() => {});
});
this.pager.collector.on('reaction', emoji => {
if (Paginator.STOP === emoji.name) {
result = { _canceled: true };
this.halt.end();
} else if (MultiSelect.DONE === emoji.name) {
result = this.pager.items;
this.halt.end();
}
this.halt.restart();
});
}
});
}
}
MultiSelect.DONE = '✅';
MultiSelect.CANCEL_TRIGGERS = [
'cancel', 'stop', 'quit'
];
MultiSelect.DONE_TRIGGERS = [
'save', 'finish', 'done'
];
module.exports = MultiSelect;

View file

@ -1,184 +0,0 @@
const EventEmitter = require('eventemitter3');
/**
* A class that creates a paging process for messages
*/
class Paginator extends EventEmitter {
/**
* @param {TrelloBot} client The client to use
* @param {Message} message The user's message to read permissions from
* @param {Object} options The options for the paginator
* @param {Array} options.items The items the paginator will display
* @param {number} [options.itemsPerPage=15] How many items a page will have
*/
constructor(client, message, { items = [], itemsPerPage = 15 } = {}) {
super();
this.messageAwaiter = client.messageAwaiter;
this.client = client;
this.collector = null;
this.items = items;
this.message = message;
this.itemsPerPage = itemsPerPage;
this.pageNumber = 1;
this.reactionsCleared = false;
this._reactBind = this._react.bind(this);
}
/**
* All pages in the paginator
* @type {Array<Array>}
*/
get pages() {
const pages = [];
let i, j, page;
for (i = 0, j = this.items.length; i < j; i += this.itemsPerPage) {
page = this.items.slice(i, i + this.itemsPerPage);
pages.push(page);
}
return pages;
}
/**
* The current page
* @type {Array}
*/
get page() {
return this.pages[this.pageNumber - 1];
}
/**
* The current page number
* @type {number}
*/
get maxPages() {
return Math.ceil(this.items.length / this.itemsPerPage);
}
/**
* Changes the page number
* @param {number} newPage The page to change to
*/
toPage(newPage) {
if (Number(newPage)){
this.pageNumber = Number(newPage);
if (this.pageNumber < 1) this.pageNumber = 1;
if (this.pageNumber > this.maxPages) this.pageNumber = this.maxPages;
}
return this;
}
/**
* Moves to the next page
*/
nextPage() {
return this.toPage(this.pageNumber + 1);
}
/**
* Moves to the previous page
*/
previousPage() {
return this.toPage(this.pageNumber - 1);
}
/**
* Whether or not this instance can paginate
* @returns {boolean}
*/
canPaginate() {
return this.message.channel.type === 1 ||
this.message.channel.permissionsOf(this.client.user.id).has('addReactions');
}
/**
* Whether or not this instance can manage messages
* @returns {boolean}
*/
canManage() {
return this.message.channel.type !== 1 &&
this.message.channel.permissionsOf(this.client.user.id).has('manageMessages');
}
/**
* Starts the reaction collector and pagination
* @param {string} userID The user's ID that started the process
* @param {number} timeout
*/
async start(userID, timeout) {
this.reactionsCleared = false;
if (this.maxPages > 1 && this.canPaginate()) {
try {
await Promise.all([
this.message.addReaction(Paginator.PREV),
this.message.addReaction(Paginator.STOP),
this.message.addReaction(Paginator.NEXT),
]);
this.collector = this.messageAwaiter.createReactionCollector(this.message, userID, timeout);
this._hookEvents();
} catch (e) {
return this.clearReactions();
}
}
}
/**
* Clears reaction from the message
*/
async clearReactions() {
if (!this.reactionsCleared) {
this.reactionsCleared = true;
this.emit('clearReactions');
try {
if (!this.canManage())
await Promise.all([
this.message.removeReaction(Paginator.NEXT).catch(() => {}),
this.message.removeReaction(Paginator.STOP).catch(() => {}),
this.message.removeReaction(Paginator.PREV).catch(() => {})
]);
else
await this.message.removeReactions().catch(() => {});
} catch (e) {
// Do nothing
}
}
}
/**
* @private
*/
_hookEvents() {
this.collector.on('reaction', this._react.bind(this));
this.collector.once('end', this.clearReactions.bind(this));
}
/**
* @private
*/
_change() {
this.emit('change', this.pageNumber);
}
/**
* @private
*/
_react(emoji, userID) {
const oldPage = this.pageNumber;
if (Paginator.PREV == emoji.name)
this.previousPage();
else if (Paginator.NEXT == emoji.name)
this.nextPage();
else if (Paginator.STOP == emoji.name)
this.collector.end();
if (this.pageNumber !== oldPage)
this._change();
if ([Paginator.PREV, Paginator.STOP, Paginator.NEXT].includes(emoji.name) && this.canManage())
this.message.removeReaction(emoji.name, userID).catch(() => {});
this.collector.restart();
}
}
Paginator.PREV = '⬅️';
Paginator.STOP = '🛑';
Paginator.NEXT = '➡️';
module.exports = Paginator;

View file

@ -1,60 +0,0 @@
const EventEmitter = require('eventemitter3');
/**
* A class that collects reactions from a message
*/
class ReactionCollector extends EventEmitter {
constructor(messageAwaiter, timeout) {
super();
this.messageAwaiter = messageAwaiter;
this.timeout = timeout;
this.interval = null;
this.ended = false;
this._endBind = this._end.bind(this);
this._start();
}
/**
* Restarts the timeout.
* @param {number} [timeout] The new timeout to halt by
*/
restart(timeout) {
if (this.ended) return;
clearTimeout(this.interval);
this.interval = setTimeout(this._endBind, timeout || this.timeout);
}
/**
* Ends the collection.
*/
end() {
if (this.ended) return;
clearTimeout(this.interval);
this._end();
}
/**
* @private
*/
_onReaction(emoji, userID) {
this.emit('reaction', emoji, userID);
this.restart();
}
/**
* @private
*/
_start() {
this.interval = setTimeout(this._endBind, this.timeout);
}
/**
* @private
*/
_end() {
this.ended = true;
this.emit('end');
}
}
module.exports = ReactionCollector;

View file

@ -1,44 +0,0 @@
const GenericPrompt = require('./GenericPrompt');
class SubMenu {
/**
* @param {TrelloBot} client The client to use
* @param {Message} message The user's message to read permissions from
* @param {Object} pagerOptions The options for the pager
*/
constructor(client, message, pagerOptions = {}) {
this.client = client;
this.message = message;
this.pagerOptions = pagerOptions;
this.prompt = new GenericPrompt(client, message, this.pagerOptions);
}
/**
* Starts the menu
* @param {string} channelID The channel to post the new message to
* @param {string} userID The user's ID that started the process
* @param {Array} menu
* @param {number} timeout
*/
async start(channelID, userID, name, menu = [], timeout = 30000) {
/*
menu = [
{
names: ['a', 'b'],
title: 'Title',
exec: (client) => ...
}
]
*/
const command = menu.find(command => command.names.includes(name ? name.toLowerCase() : null));
if (!command) {
this.prompt.pager.items = menu;
this.prompt.pager.displayFunc = (item, _, ai) => `\`[${ai + 1}]\` ${item.title}`;
const chosenCommand = await this.prompt.choose(channelID, userID, timeout);
if (!chosenCommand) return;
return chosenCommand.exec(this.client);
} else return command.exec(this.client);
}
}
module.exports = SubMenu;

View file

@ -1,363 +0,0 @@
const fetch = require('node-fetch');
const config = require('config');
const fs = require('fs');
const path = require('path');
/**
* Represents the utilities for the bot
* @typedef {Object} Util
*/
const Util = module.exports = {};
/**
* Iterates through each key of an object
* @memberof Util.
*/
Util.keyValueForEach = (obj, func) => Object.keys(obj).map(key => func(key, obj[key]));
/**
* Randomness generator
* @memberof Util.
*/
Util.Random = {
int(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
bool() {
return Util.Random.int(0, 1) === 1;
},
array(array) {
return array[Util.Random.int(0, array.length - 1)];
},
shuffle(array) {
return array.sort(() => Math.random() - 0.5);
},
};
/**
* Prefix-related functions
* @memberof Util.
*/
Util.Prefix = {
regex(client, prefixes = null) {
if (!prefixes)
prefixes = [config.prefix];
return new RegExp(`^((?:<@!?${client.user.id}>|${
prefixes.map(prefix => Util.Prefix.escapeRegex(prefix)).join('|')})\\s?)(\\n|.)`, 'i');
},
strip(message, client, prefixes) {
return message.content.replace(
Util.Prefix.regex(client, prefixes), '$2').replace(/\s\s+/g, ' ').trim();
},
escapeRegex(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}
};
/**
* Command permission parsers
* @memberof Util.
*/
Util.CommandPermissions = {
attach: (client, message) => message.channel.type === 1 ||
message.channel.permissionsOf(client.user.id).has('attachFiles'),
embed: (client, message) => message.channel.type === 1 ||
message.channel.permissionsOf(client.user.id).has('embedLinks'),
emoji: (client, message) => message.channel.type === 1 ||
message.channel.permissionsOf(client.user.id).has('externalEmojis'),
guild: (_, message) => !!message.guildID,
elevated: (_, message) => config.elevated.includes(message.author.id),
curator: (client, message) => {
const member = message.guildID ? message.member :
client.guilds.get(config.guildID).members.get(message.author.id);
const roles = Array.isArray(config.curatorRoleID) ? config.curatorRoleID : [config.curatorRoleID];
if (!member) return false;
if (Util.CommandPermissions.elevated(client, message)) return true;
return roles.map(r => member.roles.includes(r)).includes(true);
},
admin: (client, message) => {
const member = message.guildID ? message.member :
client.guilds.get(config.guildID).members.get(message.author.id);
const roles = Array.isArray(config.adminRoleID) ? config.adminRoleID : [config.adminRoleID];
if (!member) return false;
if (Util.CommandPermissions.elevated(client, message)) return true;
return roles.map(r => member.roles.includes(r)).includes(true);
},
curatorOrAdmin: (client, message) => {
const member = message.guildID ? message.member :
client.guilds.get(config.guildID).members.get(message.author.id);
const roles = [
...(Array.isArray(config.adminRoleID) ? config.adminRoleID : [config.adminRoleID]),
...(Array.isArray(config.curatorRoleID) ? config.curatorRoleID : [config.curatorRoleID]),
];
if (!member) return false;
if (Util.CommandPermissions.elevated(client, message)) return true;
return roles.map(r => member.roles.includes(r)).includes(true);
},
trustedOrAdmin: (client, message) => {
const member = message.guildID ? message.member :
client.guilds.get(config.guildID).members.get(message.author.id);
const roles = [
...(Array.isArray(config.adminRoleID) ? config.adminRoleID : [config.adminRoleID]),
...(Array.isArray(config.trustedRoleID) ? config.trustedRoleID : [config.trustedRoleID]),
];
if (!member) return false;
if (Util.CommandPermissions.elevated(client, message)) return true;
return roles.map(r => member.roles.includes(r)).includes(true);
},
};
/**
* Creates a module that makes emoji fallbacks
* @memberof Util.
*/
Util.emojiFallback = ({ message, client }) => {
return (id, fallback, animated = false) => {
if (Util.CommandPermissions.emoji(client, message))
return `<${animated ? 'a' : ''}:_:${id}>`;
else return fallback;
};
};
/**
* Cuts off text to a limit
* @memberof Util.
* @param {string} text
* @param {number} limit
*/
Util.cutoffText = (text, limit = 2000) => {
return text.length > limit ? text.slice(0, limit - 1) + '…' : text;
};
/**
* Cuts off an array of text to a limit
* @memberof Util.
* @param {Array<string>} texts
* @param {number} limit
* @param {number} rollbackAmount Amount of items to roll back when the limit has been hit
*/
Util.cutoffArray = (texts, limit = 2000, rollbackAmount = 1, paddingAmount = 1) => {
let currLength = 0;
for (let i = 0; i < texts.length; i++) {
const text = texts[i];
currLength += text.length + paddingAmount;
if (currLength > limit) {
const clampedRollback = rollbackAmount > i ? i : rollbackAmount;
return texts.slice(0, (i + 1) - clampedRollback);
}
}
return texts;
};
/**
* Resolve argument to a user ID
* @memberof Util.
* @param {string} arg
* @returns {?string}
*/
Util.resolveToUserID = (arg) => {
if (/^\d{17,18}$/.test(arg))
return arg;
else if (/^<@!?\d{17,18}>$/.test(arg))
return arg.replace(/^<@!?(\d{17,18})>$/, '$1');
else return null;
};
/**
* Resolve argument to a claim ID
* @memberof Util.
* @param {string} arg
* @returns {?string}
*/
Util.resolveToClaimID = (arg) => {
if (/^[a-f0-9]{40}$/.test(arg))
return arg;
else if (/^lbry:\/\/@?[\w-]+#([a-f0-9]{40})$/.test(arg))
return arg.replace(/^lbry:\/\/@?[\w-]+#([a-f0-9]{40})$/, '$1');
else return null;
};
/**
* Make a promise that resolves after some time
* @memberof Util.
* @param {string} arg
* @returns {?string}
*/
Util.halt = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* Hastebin-related functions
* @memberof Util.
*/
Util.Hastebin = {
async autosend(content, message) {
if (content.length > 2000) {
const haste = await Util.Hastebin.post(content);
if (haste.ok)
return message.channel.createMessage(`<https://hastebin.com/${haste.key}.md>`);
else
return message.channel.createMessage({}, {
name: 'output.txt',
file: new Buffer(content)
});
} else return message.channel.createMessage(content);
},
/**
* Post text to hastebin
* @param {string} content - The content to upload
*/
async post(content) {
const haste = await fetch('https://hastebin.com/documents', {
method: 'POST',
body: content
});
if (haste.status >= 400)
return {
ok: false,
status: haste.status
};
else {
const hasteInfo = await haste.json();
return {
ok: true,
key: hasteInfo.key
};
}
}
};
/**
* LBRY-related utility
* @memberof Util.
*/
Util.LBRY = {
async syncPairs(client) {
const response = await client.lbry.listAccounts({ page_size: await Util.LBRY.getAccountCount(client) });
const accounts = await response.json();
let syncedAccounts = 0;
for (const account of accounts.result.items) {
if (/\d{17,19}/.test(account.name)) {
if (await client.sqlite.get(account.name)) continue;
await client.sqlite.pair(account.name, account.id);
syncedAccounts++;
}
}
return syncedAccounts;
},
async findSDKAccount(client, fn) {
const response = await client.lbry.listAccounts({ page_size: await Util.LBRY.getAccountCount(client) });
const accounts = await response.json();
return accounts.result.items.find(fn);
},
async findOrCreateAccount(client, discordID, create = true) {
// Check SQLite
const pair = await client.sqlite.get(discordID);
if (pair)
return { accountID: pair.lbryID };
// Check accounts via SDK
const foundAccount = await Util.LBRY.findSDKAccount(client, account => account.name === discordID);
if (foundAccount) {
await client.sqlite.pair(discordID, foundAccount.id);
return { accountID: foundAccount.id };
}
// Create account if not found
if (create) {
const newAccount = await Util.LBRY.createAccount(client, discordID);
return {
accountID: newAccount.account.result.id,
txID: newAccount.transaction.result.txid,
newAccount: true
};
} else return { accountID: null };
},
async getAccountCount(client) {
const response = await client.lbry.listAccounts({ page_size: 1 }).then(r => r.json());
return response.result.total_items;
},
async getSupportsCount(client, accountID) {
const response = await client.lbry.listSupports({ accountID, page_size: 1 }).then(r => r.json());
return response.result.total_items;
},
async createAccount(client, discordID) {
console.info('Creating account for user', discordID);
const account = await client.lbry.createAccount(discordID).then(r => r.json());
await client.sqlite.pair(discordID, account.result.id);
console.info('Created pair', discordID, account.result.id);
const response = await client.lbry.fundAccount({ to: account.result.id, amount: config.startingBalance });
const transaction = await response.json();
console.info('Funded account', account.result.id, transaction.result.txid);
return { account, transaction };
},
ensureDecimal(str) {
const num = parseFloat(str);
if (isNaN(num)) return null;
return Number.isInteger(num) ? `${num}.0` : num.toString();
},
async deleteAccount(client, discordID, lbryID) {
// Backup the wallet before doing any delete function
try {
Util.LBRY.backupWallet();
} catch (err) {
console.error('Error occurred while backing up wallet file!');
console.error(err);
throw err;
}
// Abandon supports
await Util.LBRY.abandonAllClaims(client, lbryID);
// Take out funds from account
const balanceResponse = await client.lbry.accountBalance(lbryID);
let amount = (await balanceResponse.json()).result.total;
while (amount >= 2) {
await client.lbry.fundAccount({ from: lbryID, everything: true, amount });
const finalBalance = await client.lbry.accountBalance(lbryID);
amount = (await finalBalance.json()).result.total;
await Util.halt(3000);
}
// Remove account from SDK & SQLite
await client.lbry.removeAccount(lbryID);
await client.sqlite.remove(discordID);
},
async abandonAllClaims(client, lbryID) {
if (!lbryID)
throw new Error('lbryID must be defined!');
const supportsCount = await Util.LBRY.getSupportsCount(client, lbryID);
const supportsResponse = await client.lbry.listSupports({
accountID: lbryID, page_size: supportsCount });
console.info(`Abandoning claims for ${lbryID} (${supportsCount})`);
const supports = (await supportsResponse.json()).result.items;
for (let i = 0, len = supports.length; i < len; i++) {
const support = supports[i];
await client.lbry.abandonSupport({ claimID: support.claim_id, accountID: lbryID });
await Util.halt(3000);
}
return { count: supports.length };
},
backupWallet() {
const wallet = fs.readFileSync(config.walletPath);
const d = new Date();
const date = [
d.getUTCFullYear(),
d.getUTCMonth().toString().padStart(2, '0'),
d.getUTCDay().toString().padStart(2, '0'),
].join('-');
const time = [
d.getUTCHours().toString().padStart(2, '0'),
d.getUTCMinutes().toString().padStart(2, '0'),
d.getUTCSeconds().toString().padStart(2, '0'),
d.getUTCMilliseconds().toString()
].join('-');
const backupName = 'default_wallet.' + date + '_' + time + '.bak';
const backupPath = path.join(config.walletBackupFolder, backupName);
fs.writeFileSync(backupPath, wallet);
console.log(`Backed up wallet file: ${backupPath}`);
}
};

70
src/util/abstracts.ts Normal file
View file

@ -0,0 +1,70 @@
import { oneLine } from 'common-tags';
import { ClientEvent, CommandContext, DexareCommand, PermissionNames } from 'dexare';
import LBRYModule from '../modules/lbry';
import WalletModule from '../modules/wallet';
export abstract class GeneralCommand extends DexareCommand {
get lbry() {
return this.client.modules.get('lbry')! as LBRYModule<any>;
}
get lbryx() {
return this.client.modules.get('lbryx')! as LBRYModule<any>;
}
get wallet() {
return this.client.modules.get('wallet')! as WalletModule<any>;
}
get embedColor(): number {
return this.client.config.embedColor;
}
hasPermission(ctx: CommandContext, event?: ClientEvent): boolean | string {
if (this.userPermissions) {
let permissionMap = event && event.has('dexare/permissionMap') ? event.get('dexare/permissionMap') : {};
permissionMap = this.client.permissions.map(
this.client.permissions.toObject(ctx.message),
this.userPermissions,
permissionMap,
event
);
if (event) event.set('dexare/permissionMap', permissionMap);
const missing = this.userPermissions.filter((perm: string) => !permissionMap[perm]);
if (missing.length > 0) {
if (missing.includes('dexare.elevated'))
return `The \`${this.name}\` command can only be used by the bot developers or elevated users.`;
else if (missing.includes('lbry.curator') || missing.includes('lbry.curatorOrAdmin'))
return `The \`${this.name}\` command can only be ran by LBRY curators.`;
else if (missing.includes('lbry.trusted') || missing.includes('lbry.trustedOrAdmin'))
return `The \`${this.name}\` command can only be ran by LBRY trusteds.`;
else if (missing.includes('lbry.admin'))
return `The \`${this.name}\` command can only be ran by LBRY admins.`;
else if (missing.includes('dexare.nsfwchannel'))
return `The \`${this.name}\` command can only be ran in NSFW channels.`;
else if (missing.includes('dexare.inguild'))
return `The \`${this.name}\` command can only be ran in guilds.`;
else if (missing.length === 1) {
return `The \`${this.name}\` command requires you to have the "${
PermissionNames[missing[0]] || missing[0]
}" permission.`;
}
return oneLine`
The \`${this.name}\` command requires you to have the following permissions:
${missing.map((perm) => PermissionNames[perm] || perm).join(', ')}
`;
}
}
return true;
}
finalize(response: any, ctx: CommandContext) {
if (
typeof response === 'string' ||
(response && response.constructor && response.constructor.name === 'Object')
)
return ctx.reply(response);
}
}

72
src/util/index.ts Normal file
View file

@ -0,0 +1,72 @@
/**
* Make a promise that resolves after some time
* @param ms The time to resolve at
*/
export function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Resolves a Discord users ID.
* @param arg The value to resolve
*/
export function resolveUser(arg: string) {
if (/^\d{17,18}$/.test(arg)) return arg;
else if (/^<@!?\d{17,18}>$/.test(arg)) return arg.replace(/^<@!?(\d{17,18})>$/, '$1');
else return null;
}
/**
* Makes sure a value is a decimal usable for the LBRY SDK.
* @param arg The value to ensure
*/
export function ensureDecimal(arg: string | number) {
const num = parseFloat(arg as any);
if (isNaN(num)) return null;
return Number.isInteger(num) ? `${num}.0` : num.toString();
}
export interface SplitOptions {
maxLength?: number;
char?: string;
prepend?: string;
append?: string;
}
/**
* Splits a string into multiple chunks at a designated character that do not exceed a specific length.
* @param text Content to split
* @param options Options controlling the behavior of the split
*/
export function splitMessage(
text: string,
{ maxLength = 2000, char = '\n', prepend = '', append = '' }: SplitOptions = {}
) {
if (text.length <= maxLength) return [text];
let splitText = [text];
if (Array.isArray(char)) {
while (char.length > 0 && splitText.some((elem) => elem.length > maxLength)) {
const currentChar = char.shift();
if (currentChar instanceof RegExp) {
// @ts-ignore
splitText = splitText.map((chunk) => chunk.match(currentChar));
} else {
// @ts-ignore
splitText = splitText.map((chunk) => chunk.split(currentChar));
}
}
} else {
splitText = text.split(char);
}
if (splitText.some((elem) => elem.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN');
const messages = [];
let msg = '';
for (const chunk of splitText) {
if (msg && (msg + char + chunk + append).length > maxLength) {
messages.push(msg + append);
msg = prepend;
}
msg += (msg && msg !== prepend ? char : '') + chunk;
}
return messages.concat(msg).filter((m) => m);
}

21
tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules",
"dist",
"testing",
"src-old"
]
}

1966
yarn.lock

File diff suppressed because it is too large Load diff