mirror of
https://github.com/LBRYFoundation/curate.git
synced 2025-08-23 17:37:25 +00:00
Start rewriting to dexare
This commit is contained in:
parent
d1a7cbead6
commit
5d4037f90a
61 changed files with 1824 additions and 4211 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
|
|
@ -1 +1,3 @@
|
|||
config/
|
||||
config/
|
||||
src-old/
|
||||
node_modules/
|
||||
|
|
37
.eslintrc.js
Normal file
37
.eslintrc.js
Normal 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'
|
||||
}
|
||||
};
|
|
@ -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
13
.gitignore
vendored
|
@ -1,6 +1,11 @@
|
|||
node_modules
|
||||
config/default.js
|
||||
config/production.js
|
||||
config/curate.sqlite
|
||||
config/*
|
||||
!config/_default.js
|
||||
*.sqlite
|
||||
package-lock.json
|
||||
.idea/
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
src-old/
|
||||
|
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 110
|
||||
}
|
|
@ -9,7 +9,6 @@ This bot allows the community of LBRY to support eachother through the [LBRY Fou
|
|||
* Pull the repo
|
||||
* Install [Node.JS LTS](https://nodejs.org/) (Currently Node v12.x)
|
||||
* Install [Yarn](https://yarnpkg.com/) (`npm install yarn -g`)
|
||||
* Install [Redis](https://redis.io/) ([quickstart](https://redis.io/topics/quickstart))
|
||||
* Install LBRY-SDK
|
||||
* 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
|
||||
|
|
|
@ -1,56 +1,71 @@
|
|||
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
|
||||
commandsPath: "./src/commands",
|
||||
// [boolean] Whether debug logs will be shown
|
||||
debug: false,
|
||||
// [number] The main embed color (#ffffff -> 0xffffff)
|
||||
embedColor: 0x15521c,
|
||||
// [string|Array<string>] The role ID(s) for curator roles
|
||||
curatorRoleID: "",
|
||||
// [string|Array<string>] The role ID(s) for trusted roles
|
||||
trustedRoleID: "",
|
||||
// [string|Array<string>] The role ID(s) for admin roles
|
||||
adminRoleID: "",
|
||||
// [string] guild_id
|
||||
guildID: "",
|
||||
// [string] sdk_url
|
||||
sdkURL: "",
|
||||
// [string] The ABSOLUTE path to the main wallet file to back up
|
||||
walletPath: "~/.lbryum/wallets/default_wallet",
|
||||
// [string] The ABSOLUTE path folder to store wallet backups after every deletion
|
||||
walletBackupFolder: "~/.lbryum_backup/",
|
||||
// [string] Amount to auto-fund upon account creation
|
||||
startingBalance: "",
|
||||
// [Object] Eris client options (https://abal.moe/Eris/docs/Client)
|
||||
discordConfig: {
|
||||
autoreconnect: true,
|
||||
allowedMentions: {
|
||||
everyone: false,
|
||||
roles: false,
|
||||
users: true
|
||||
|
||||
// Dexare config
|
||||
dexare: {
|
||||
// [string] The token for the bot
|
||||
token: "",
|
||||
// [string] The prefix for the bot
|
||||
prefix: "c!",
|
||||
// [boolean?] Whether to use the bots mention as a prefix
|
||||
mentionPrefix: true,
|
||||
// [Array<string>] An array of elevated IDs, giving them access to developer commands
|
||||
elevated: [],
|
||||
// [number] The main embed color (#ffffff -> 0xffffff)
|
||||
embedColor: 0x15521c,
|
||||
// [string|Array<string>] The role ID(s) for curator roles
|
||||
curatorRoles: "",
|
||||
// [string|Array<string>] The role ID(s) for trusted roles
|
||||
trustedRoles: "",
|
||||
// [string|Array<string>] The role ID(s) for admin roles
|
||||
adminRoles: "",
|
||||
// [string] The ID of the main Discord guild
|
||||
guildID: "",
|
||||
|
||||
|
||||
// [Object] Eris client options (https://abal.moe/Eris/docs/Client)
|
||||
erisConfig: {
|
||||
autoreconnect: true,
|
||||
allowedMentions: {
|
||||
everyone: false,
|
||||
roles: false,
|
||||
users: true
|
||||
},
|
||||
maxShards: "auto",
|
||||
messageLimit: 0,
|
||||
intents: [
|
||||
"guilds",
|
||||
"guildMessages",
|
||||
"guildMessageReactions",
|
||||
"directMessages",
|
||||
"directMessageReactions"
|
||||
]
|
||||
},
|
||||
maxShards: "auto",
|
||||
messageLimit: 0,
|
||||
intents: [
|
||||
"guilds",
|
||||
"guildEmojis",
|
||||
"guildMessages",
|
||||
"guildMessageReactions",
|
||||
"directMessages",
|
||||
"directMessageReactions"
|
||||
]
|
||||
},
|
||||
// [Object] Redis config
|
||||
redis: {
|
||||
host: "localhost",
|
||||
port: 6379,
|
||||
password: "",
|
||||
prefix: "lbrycurate:"
|
||||
|
||||
logger: {
|
||||
level: 'debug'
|
||||
},
|
||||
|
||||
cron: {
|
||||
loadFolder: './src/crons'
|
||||
},
|
||||
|
||||
lbry: {
|
||||
// [string] The SDK url to request from
|
||||
sdkURL: ""
|
||||
},
|
||||
|
||||
lbryx: {
|
||||
// [string?] Amount to auto-fund upon account creation
|
||||
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/",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
47
package.json
47
package.json
|
@ -1,29 +1,44 @@
|
|||
{
|
||||
"name": "lbry-curate",
|
||||
"version": "0.1.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Support the LBRY Community through Discord!",
|
||||
"main": "src/bot.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/bot.js",
|
||||
"eslint": "eslint ./src",
|
||||
"eslint:fix": "eslint ./src --fix"
|
||||
"start": "cd dist && node index.js",
|
||||
"start:prod": "cd dist && NODE_ENV=production node index.js",
|
||||
"build": "tsc",
|
||||
"dev": "devScript",
|
||||
"lint": "npx eslint --ext .ts ./src",
|
||||
"lint:fix": "npx eslint --ext .ts ./src --fix"
|
||||
},
|
||||
"devScript": {
|
||||
"depCheck": false
|
||||
},
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"cat-loggr": "^1.2.2",
|
||||
"@dexare/cron": "^1.0.0",
|
||||
"@dexare/logger": "^1.0.0",
|
||||
"common-tags": "^1.8.0",
|
||||
"config": "^3.3.6",
|
||||
"eris": "^0.15.1",
|
||||
"dexare": "^2.0.1",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"fuzzy": "^0.1.3",
|
||||
"moment": "^2.29.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"redis": "^3.1.2",
|
||||
"require-reload": "^0.2.2",
|
||||
"sequelize": "^6.6.2",
|
||||
"sqlite3": "^5.0.2"
|
||||
"quick.db": "^7.1.3",
|
||||
"steno": "^2.0.0"
|
||||
},
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
|
4
pm2.json
4
pm2.json
|
@ -2,8 +2,8 @@
|
|||
"apps": [
|
||||
{
|
||||
"name": "LBRYCurate",
|
||||
"script": "node",
|
||||
"args": "src/bot.js"
|
||||
"script": "yarn",
|
||||
"args": "start:prod"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
151
src/bot.js
151
src/bot.js
|
@ -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
97
src/bot.ts
Normal 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
|
@ -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`'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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> …'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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] …'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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!'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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.'
|
||||
}; }
|
||||
};
|
|
@ -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>'
|
||||
}; }
|
||||
};
|
|
@ -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]'
|
||||
}; }
|
||||
};
|
10
src/crons/flushCache.cron.ts
Normal file
10
src/crons/flushCache.cron.ts
Normal 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();
|
||||
}
|
139
src/database.js
139
src/database.js
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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
9
src/index.ts
Normal 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();
|
|
@ -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;
|
|
@ -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 }));
|
||||
}
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
363
src/util.js
363
src/util.js
|
@ -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
70
src/util/abstracts.ts
Normal 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
72
src/util/index.ts
Normal 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
21
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue