mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-06-17 02:29:10 +00:00
absolutely massive typescript porting time
This commit is contained in:
parent
01f238f515
commit
30d08a630f
159 changed files with 2563 additions and 3861 deletions
249
packages/roleypoly-bot/src/Bot.ts
Normal file
249
packages/roleypoly-bot/src/Bot.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
// tslint:disable: no-floating-promises
|
||||
|
||||
import Eris, { Message, TextChannel, Guild } from 'eris'
|
||||
// import RPCClient from '@roleypoly/rpc-client'
|
||||
import randomEmoji from './random-emoji'
|
||||
import DMCommands from './commands/dm'
|
||||
import TextCommands from './commands/text'
|
||||
import RootCommands from './commands/root'
|
||||
import { Command } from './commands/_types'
|
||||
import retry from 'async-retry'
|
||||
|
||||
import logger from './logger'
|
||||
const log = logger(__filename)
|
||||
|
||||
export type BotConfig = {
|
||||
sharedSecret: string,
|
||||
botToken: string,
|
||||
rootUsers: Set<string>,
|
||||
appUrl: string,
|
||||
logChannel: string
|
||||
}
|
||||
|
||||
export default class Bot {
|
||||
config: BotConfig = {
|
||||
sharedSecret: process.env.SHARED_SECRET || '',
|
||||
botToken: process.env.DISCORD_BOT_TOKEN || '',
|
||||
rootUsers: new Set((process.env.ROOT_USERS || '').split(',')),
|
||||
appUrl: process.env.APP_URL || '',
|
||||
logChannel: process.env.LOG_CHANNEL || ''
|
||||
}
|
||||
|
||||
client: Eris.Client
|
||||
|
||||
// $RPC: RPCClient
|
||||
|
||||
// rpc: typeof RPCClient
|
||||
|
||||
commandCheck: RegExp = new RegExp(`^<@!?${process.env.DISCORD_CLIENT_ID}>`)
|
||||
|
||||
constructor (config: Partial<BotConfig> = {}) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...config
|
||||
}
|
||||
|
||||
this.client = new Eris.Client(this.config.botToken, {
|
||||
disableEveryone: true,
|
||||
maxShards: 'auto',
|
||||
messageLimit: 10,
|
||||
disableEvents: {
|
||||
CHANNEL_PINS_UPDATE: true,
|
||||
USER_SETTINGS_UPDATE: true,
|
||||
USER_NOTE_UPDATE: true,
|
||||
RELATIONSHIP_ADD: true,
|
||||
RELATIONSHIP_REMOVE: true,
|
||||
GUILD_BAN_ADD: true,
|
||||
GUILD_BAN_REMOVE: true,
|
||||
TYPING_START: true,
|
||||
MESSAGE_UPDATE: true,
|
||||
MESSAGE_DELETE: true,
|
||||
MESSAGE_DELETE_BULK: true,
|
||||
VOICE_STATE_UPDATE: true
|
||||
}
|
||||
})
|
||||
|
||||
// this.$RPC = new RPCClient({ forceDev: false, retry: true })
|
||||
// this.rpc = this.$RPC.withBotAuth(this.config.sharedSecret)
|
||||
this.botPing()
|
||||
|
||||
if (this.config.sharedSecret === '') {
|
||||
log.fatal('configuration incomplete: SHARED_SECRET missing')
|
||||
}
|
||||
|
||||
if (this.config.botToken === '') {
|
||||
log.fatal('configuration incomplete: DISCORD_BOT_TOKEN missing')
|
||||
}
|
||||
|
||||
// <@botid> AND <@!botid> both are valid mentions.
|
||||
retry(() => { this.commandCheck = new RegExp(`^<@!?${this.client.user.id}>`) })
|
||||
}
|
||||
|
||||
isRoot (u: string) {
|
||||
return this.config.rootUsers.has(u)
|
||||
}
|
||||
|
||||
sendErrorLog (msg: Message, err: Error) {
|
||||
if (this.config.logChannel === '') {
|
||||
return
|
||||
}
|
||||
|
||||
this.client.createMessage(this.config.logChannel, `command errored.`)
|
||||
}
|
||||
|
||||
botPing = async () => {
|
||||
try {
|
||||
// await this.rpc.botPing()
|
||||
} catch (e) {
|
||||
log.fatal('bot failed to connect to RPC server.', e)
|
||||
}
|
||||
}
|
||||
|
||||
buildLogMsg (msg: Message, startTime: number) {
|
||||
const endTime = Date.now()
|
||||
return `${msg.channel.id} [${msg.channel.type === 0 ? 'TEXT' : 'DM'}] | ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}) {${endTime - startTime}ms}: ${msg.cleanContent}`
|
||||
}
|
||||
|
||||
gatherMsgInfo (msg: Message) {
|
||||
const { id, author, channel, type } = msg
|
||||
const o = {
|
||||
id,
|
||||
type: channel.type,
|
||||
author: {
|
||||
id: author.id,
|
||||
name: `${author.username}#${author.discriminator}`
|
||||
},
|
||||
channel: {
|
||||
id: channel.id
|
||||
// name: channel.name || ''
|
||||
},
|
||||
guild: {}
|
||||
}
|
||||
|
||||
try {
|
||||
if (type === 0) {
|
||||
const { id, name } = (msg.channel as TextChannel).guild
|
||||
o.guild = {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn('type was 0 but no guild info')
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// this is a very silly O(n) matcher. i bet we can do better later.
|
||||
findCmdIn (text: string, items: Set<Command>): { cmd: Command, matches: string[] } | undefined {
|
||||
for (const cmd of items) {
|
||||
const matches = text.match(cmd.regex)
|
||||
if (matches !== null) {
|
||||
return { cmd, matches }
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async tryCommand (msg: Message, text: string, startTime: number, ...sets: Array<Set<Command> | null>): Promise<boolean> {
|
||||
for (const set of sets) {
|
||||
if (set === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const cmd of set) {
|
||||
const matches = text.match(cmd.regex)
|
||||
if (matches !== null) {
|
||||
// handle!
|
||||
try {
|
||||
const result = await cmd.handler(this, msg, matches.slice(1))
|
||||
if (result !== undefined) {
|
||||
await msg.channel.createMessage(result)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('command failed.', msg, e)
|
||||
this.sendErrorLog(msg, e)
|
||||
msg.channel.createMessage(`:x: Something went terribly wrong!`)
|
||||
}
|
||||
|
||||
log.bot(this.buildLogMsg(msg, startTime))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async handleChannelMessage (msg: Message) {
|
||||
const startTime = Date.now()
|
||||
|
||||
// only react if there's a mention of myself.
|
||||
const check = msg.mentions.find(v => v.id === this.client.user.id)
|
||||
if (check === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// if root, we'll mixin some new commands
|
||||
const isRoot = this.isRoot(msg.author.id)
|
||||
|
||||
const text = msg.content
|
||||
|
||||
// check what consititutes a command prefix, <@botid> AND <@!botid> both are valid mentions.
|
||||
// this is reflected in the regex.
|
||||
if (this.commandCheck.test(text)) {
|
||||
const cmd = text.replace(this.commandCheck, '').trim()
|
||||
if (cmd !== '') { // empty? probably not good.
|
||||
const success = await this.tryCommand(msg, cmd, startTime, TextCommands, isRoot ? RootCommands : null)
|
||||
|
||||
if (success === true) {
|
||||
log.debug('reached success', msg.content)
|
||||
} else {
|
||||
log.debug('no success.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const channel: TextChannel = msg.channel as TextChannel
|
||||
channel.createMessage(`${randomEmoji()} Assign your roles here! ${this.config.appUrl}/s/${channel.guild.id}`)
|
||||
}
|
||||
|
||||
async handleDMMessage (msg: Message) {
|
||||
const startTime = Date.now()
|
||||
const isRoot = this.isRoot(msg.author.id)
|
||||
await this.tryCommand(msg, msg.content, startTime, DMCommands, isRoot ? RootCommands : null)
|
||||
}
|
||||
|
||||
handleMessage = (msg: Message) => {
|
||||
if (msg.author.bot) { // no bots
|
||||
return
|
||||
}
|
||||
|
||||
switch (msg.channel.type) {
|
||||
case 0: // text channel
|
||||
return this.handleChannelMessage(msg)
|
||||
case 1: // dm channel
|
||||
return this.handleDMMessage(msg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
async syncGuild (type: string, guild: Guild) {
|
||||
// await this.rpc.syncGuild(type, guild.id)
|
||||
}
|
||||
|
||||
async start () {
|
||||
this.client.on('messageCreate', this.handleMessage)
|
||||
|
||||
const guildSyncTriggers = ['guildCreate', 'guildDelete', 'guildRoleAdd', 'guildRoleDelete', 'guildRoleUpdate']
|
||||
for (const trigger of guildSyncTriggers) {
|
||||
this.client.on(trigger, this.syncGuild.bind(this, trigger))
|
||||
}
|
||||
|
||||
await this.client.connect()
|
||||
log.info('started bot')
|
||||
}
|
||||
}
|
11
packages/roleypoly-bot/src/commands/_types.ts
Normal file
11
packages/roleypoly-bot/src/commands/_types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Message } from 'eris'
|
||||
import Bot from '../Bot'
|
||||
|
||||
export type Command = {
|
||||
regex: RegExp,
|
||||
usage: string,
|
||||
description: string,
|
||||
handler: (bot: Bot, message: Message, matches: string[]) => Promise<string | void> | string | void
|
||||
}
|
||||
|
||||
export type CommandSet = Set<Command>
|
30
packages/roleypoly-bot/src/commands/dm.ts
Normal file
30
packages/roleypoly-bot/src/commands/dm.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import type { Command } from './_types'
|
||||
import { withTyping } from '../utils'
|
||||
import type Bot from '../Bot'
|
||||
import type { Message } from 'eris'
|
||||
|
||||
const cmds: Command[] = [
|
||||
{
|
||||
regex: /^\blog ?in|auth\b/,
|
||||
usage: 'login',
|
||||
description: 'responds with a login token and link',
|
||||
|
||||
handler: withTyping(async (bot: Bot, msg: Message) => {
|
||||
const chall = await bot.rpc.issueAuthChallenge(msg.author.id)
|
||||
return chall
|
||||
})
|
||||
},
|
||||
{
|
||||
regex: /^\blog ?out\b/,
|
||||
usage: 'logout',
|
||||
description: 'removes all sessions',
|
||||
|
||||
handler: withTyping(async (bot: Bot, msg: Message) => {
|
||||
await bot.rpc.removeUserSessions(msg.author.id)
|
||||
return `I've logged you out!`
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
export default new Set<Command>(cmds)
|
30
packages/roleypoly-bot/src/commands/root.ts
Normal file
30
packages/roleypoly-bot/src/commands/root.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import type { Command } from './_types'
|
||||
import type Bot from '../Bot'
|
||||
import type { Message } from 'eris'
|
||||
import { withTyping } from '../utils'
|
||||
|
||||
const cmds: Command[] = [
|
||||
{
|
||||
regex: /^remove sessions for/,
|
||||
usage: 'remove sessions for <mention>',
|
||||
description: 'removes all sessions for a given user.',
|
||||
|
||||
handler: withTyping(async (bot: Bot, msg: Message) => {
|
||||
const u = msg.mentions[0]
|
||||
await bot.rpc.removeUserSessions(u.id)
|
||||
return `${u.mention} should no longer be logged in on any device.`
|
||||
})
|
||||
},
|
||||
{
|
||||
regex: /^always error/,
|
||||
usage: 'always error',
|
||||
description: 'creates an error',
|
||||
|
||||
handler: () => {
|
||||
throw new Error('not an error, this is actually a success.')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default new Set<Command>(cmds)
|
18
packages/roleypoly-bot/src/commands/text.ts
Normal file
18
packages/roleypoly-bot/src/commands/text.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// @flow
|
||||
import type { Command } from './_types'
|
||||
import type Bot from '../Bot'
|
||||
import type { Message } from 'eris'
|
||||
|
||||
const cmds: Command[] = [
|
||||
{
|
||||
regex: /^hi/,
|
||||
usage: 'hi',
|
||||
description: 'says hello',
|
||||
|
||||
handler: (bot: Bot, msg: Message) => {
|
||||
msg.channel.createMessage('Hi there!')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default new Set<Command>(cmds)
|
6
packages/roleypoly-bot/src/index.ts
Normal file
6
packages/roleypoly-bot/src/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
import 'dotenv/config'
|
||||
import Bot from './Bot'
|
||||
|
||||
const B = new Bot()
|
||||
B.start().catch()
|
70
packages/roleypoly-bot/src/logger.ts
Normal file
70
packages/roleypoly-bot/src/logger.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import chalk from 'chalk'
|
||||
|
||||
export class Logger {
|
||||
debugOn: boolean
|
||||
name: string
|
||||
quietSql: boolean
|
||||
|
||||
constructor (name: string, debugOverride: boolean = false) {
|
||||
this.name = name
|
||||
this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride
|
||||
this.quietSql = (process.env.DEBUG_SQL !== 'true')
|
||||
}
|
||||
|
||||
fatal (text: string, ...data: any) {
|
||||
this.error(text, data)
|
||||
|
||||
if (typeof data[data.length - 1] === 'number') {
|
||||
process.exit(data[data.length - 1])
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
error (text: string, ...data: any) {
|
||||
console.error(chalk.red.bold(`ERR ${this.name}:`) + `\n ${text}`, data)
|
||||
}
|
||||
|
||||
warn (text: string, ...data: any) {
|
||||
console.warn(chalk.yellow.bold(`WARN ${this.name}:`) + `\n ${text}`, data)
|
||||
}
|
||||
|
||||
notice (text: string, ...data: any) {
|
||||
console.log(chalk.cyan.bold(`NOTICE ${this.name}:`) + `\n ${text}`, data)
|
||||
}
|
||||
|
||||
info (text: string, ...data: any) {
|
||||
console.info(chalk.blue.bold(`INFO ${this.name}:`) + `\n ${text}`, data)
|
||||
}
|
||||
|
||||
bot (text: string, ...data: any) {
|
||||
console.log(chalk.yellowBright.bold(`BOT CMD:`) + `\n ${text}`, data)
|
||||
}
|
||||
|
||||
deprecated (text: string, ...data: any) {
|
||||
console.warn(chalk.yellowBright(`DEPRECATED ${this.name}:`) + `\n ${text}`, data)
|
||||
console.trace()
|
||||
}
|
||||
|
||||
request (text: string, ...data: any) {
|
||||
console.info(chalk.green.bold(`HTTP ${this.name}:`) + `\n ${text}`)
|
||||
}
|
||||
|
||||
debug (text: string, ...data: any) {
|
||||
if (this.debugOn) {
|
||||
console.log(chalk.gray.bold(`DEBUG ${this.name}:`) + `\n ${text}`, data)
|
||||
}
|
||||
}
|
||||
|
||||
sql (logger: Logger, ...data: any) {
|
||||
if (logger.debugOn && !logger.quietSql) {
|
||||
console.log(chalk.bold('DEBUG SQL:\n '), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default (pathname: string) => {
|
||||
const name = pathname.replace(__dirname, '').replace('.js', '')
|
||||
return new Logger(name)
|
||||
}
|
15
packages/roleypoly-bot/src/random-emoji.ts
Normal file
15
packages/roleypoly-bot/src/random-emoji.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const emojis = [
|
||||
':beginner:',
|
||||
':beginner:',
|
||||
':beginner:',
|
||||
':beginner:',
|
||||
':beginner:',
|
||||
':beginner:',
|
||||
':sunglasses:',
|
||||
':gay_pride_flag:',
|
||||
':gift_heart:',
|
||||
// ':lobster:',
|
||||
':squid:'
|
||||
]
|
||||
|
||||
export default () => { return emojis[Math.floor(Math.random() * emojis.length)] }
|
8
packages/roleypoly-bot/src/utils.ts
Normal file
8
packages/roleypoly-bot/src/utils.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import Bot from './Bot'
|
||||
import { Message } from 'eris'
|
||||
|
||||
export const withTyping = (fn: Function) => async (bot: Bot, msg: Message, ...args: any[]) => {
|
||||
await msg.channel.sendTyping()
|
||||
return fn(bot, msg, ...args)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue