1
0
Fork 0

add source bot to command context, add user lookup to bot, allow giving karma to others

This commit is contained in:
Sean Sube 2022-12-17 09:40:52 -06:00
parent 0e7b153797
commit 3e483d4a31
8 changed files with 79 additions and 32 deletions

View File

@ -53,9 +53,13 @@ test: build
# image-building targets
image:
podman build -f Containerfile .
podman build -t docker-push.artifacts.apextoaster.com/ssube/conan-discord:main -f Containerfile .
image-local: ci
podman pull docker-push.artifacts.apextoaster.com/ssube/conan-discord:main
$(MAKE) image
podman push docker-push.artifacts.apextoaster.com/ssube/conan-discord:main
# run targets
run: build
node out/src/index.js

View File

@ -1,7 +1,7 @@
import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Client, Events, GatewayIntentBits, Message, Partials } from 'discord.js';
import { Command, CommandContext } from '../command/index.js';
import { Author, Command, CommandContext } from '../command/index.js';
import { catchAndLog } from '../utils/async.js';
import { matchCommands } from '../utils/command.js';
import { Bot, BotContext } from './index.js';
@ -65,6 +65,7 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
const context: CommandContext = {
...botContext,
bot,
command,
};
@ -88,7 +89,29 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
catchAndLog(onMessage(message), logger, 'error handling message');
});
await client.login(args.discordToken);
const bot: Bot = {
async connect() {
await client.login(args.discordToken);
return true;
},
destroy() {
client.destroy();
},
async getUser(ident: string): Promise<Author> {
const match = /<@(\d+)>/.exec(ident);
return client;
if (match) {
const [_full, snowflake] = Array.from(match);
const user = await client.users.fetch(snowflake);
return {
discriminator: user.discriminator,
username: user.username,
};
} else {
throw new Error('invalid user ident');
}
},
};
return bot;
}

View File

@ -1,7 +1,7 @@
import Logger from 'bunyan';
import { ParsedArgs } from '../args.js';
import { Command } from '../command/index.js';
import { Author, Command } from '../command/index.js';
import { DataLayer } from '../data/index.js';
import { RconClient } from '../utils/rcon.js';
import { discordConnect } from './discord.js';
@ -13,8 +13,10 @@ import { readlineConnect } from './readline.js';
* @public
*/
export interface Bot {
// login(args: ParsedArgs): Promise<boolean>;
connect(): Promise<boolean>;
destroy(): void;
getUser(ident: string): Promise<Author | undefined>;
}
/**

View File

@ -5,17 +5,22 @@ import { createInterface } from 'node:readline/promises';
import { Author, Command, CommandContext, Message } from '../command/index.js';
import { catchAndLog } from '../utils/async.js';
import { LOCAL_DISCRIMINATOR, matchCommands } from '../utils/command.js';
import { BotContext } from './index.js';
import { Bot, BotContext } from './index.js';
/**
* Readline-only methods.
*
* @public
*/
export interface ReadlineBot {
export interface ReadlineBot extends Bot {
destroy(): void;
}
export const READLINE_AUTHOR: Author = {
discriminator: LOCAL_DISCRIMINATOR,
username: 'readline',
};
/**
* Start the readline bot interface.
*
@ -40,26 +45,22 @@ export async function readlineConnect(botContext: BotContext, commands: Readonly
const context: CommandContext = {
...botContext,
bot,
command,
};
const author: Author = {
discriminator: LOCAL_DISCRIMINATOR,
username: 'readline',
};
if (doesExist(command.auth)) {
const allowed = await command.auth(author, context);
logger.debug({ allowed, author, command }, 'command auth check');
const allowed = await command.auth(READLINE_AUTHOR, context);
logger.debug({ allowed, author: READLINE_AUTHOR, command }, 'command auth check');
if (allowed === false) {
logger.warn({ author, command }, 'author attempted to use unauthorized command');
logger.warn({ author: READLINE_AUTHOR, command }, 'author attempted to use unauthorized command');
continue;
}
}
const message: Message = {
author,
author: READLINE_AUTHOR,
content: line,
async reply(reply: string) {
logger.info({ reply }, 'reply from command');
@ -82,11 +83,19 @@ export async function readlineConnect(botContext: BotContext, commands: Readonly
});
rl.setPrompt('> ');
rl.prompt();
return {
const bot: Bot = {
async connect() {
rl.prompt();
return true;
},
destroy() {
rl.close();
},
async getUser() {
return READLINE_AUTHOR;
},
};
return bot;
}

View File

@ -1,6 +1,7 @@
import Logger from 'bunyan';
import { ParsedArgs } from '../args.js';
import { Bot } from '../bot/index.js';
import { DataLayer } from '../data/index.js';
import { RconClient } from '../utils/rcon.js';
import { rconSQL } from './admin/rcon-sql.js';
@ -51,6 +52,7 @@ export interface Command {
*/
export interface CommandContext {
args: ParsedArgs;
bot: Bot;
command: Command;
data: DataLayer;
logger: Logger;

View File

@ -1,3 +1,4 @@
import { mustDefault } from '@apextoaster/js-utils';
import { DataTable, intValue } from '../data/index.js';
import { parseBody } from '../utils/command.js';
import { Command, CommandContext, Message } from './index.js';
@ -28,17 +29,21 @@ export const karma: Command = {
},
});
context.logger.debug({ body, args }, 'karma');
const target = args._[0];
const targetAuthor = await context.bot.getUser(target);
const karmaTarget = mustDefault(targetAuthor, msg.author);
context.logger.debug({ body, args, karmaTarget }, 'karma target');
const argsAmount = args.amount;
const bodyAmount = countKarma(body);
const change = Math.max(Math.min(argsAmount + bodyAmount, KARMA_LIMITS.max), KARMA_LIMITS.min);
context.logger.debug({ change, karmaTarget }, 'changing karma');
const prev = await context.data.get(DataTable.STATE, msg.author, karma.name);
const prev = await context.data.get(DataTable.STATE, karmaTarget, karma.name);
const prevAmount = intValue(prev, 'amount');
const nextAmount = prevAmount + change;
await context.data.set(DataTable.STATE, msg.author, karma.name, {
await context.data.set(DataTable.STATE, karmaTarget, karma.name, {
amount: nextAmount.toFixed(0),
});

View File

@ -4,7 +4,6 @@ import { createLogger, DEBUG } from 'bunyan';
import { APP_NAME, parseArgs } from './args.js';
import { Bot, BotContext, BOTS } from './bot/index.js';
import { COMMANDS } from './command/index.js';
import { MemoryDataLayer } from './data/memory.js';
import { SqliteDataLayer } from './data/sqlite.js';
import { rconConnect } from './utils/rcon.js';
@ -44,13 +43,15 @@ export async function main(argv: Array<string>): Promise<ExitCode> {
rcon,
};
for (const bot of args.bots) {
const ctor = BOTS[bot];
for (const botName of args.bots) {
const ctor = BOTS[botName];
if (typeof ctor === 'function') {
logger.info({ bot }, 'starting bot');
bots.push(await ctor(botContext, COMMANDS));
logger.info({ botName }, 'starting bot');
const bot = await ctor(botContext, COMMANDS);
await bot.connect();
bots.push(bot);
} else {
logger.warn({ bot }, 'unknown bot');
logger.warn({ botName }, 'unknown bot');
}
}

View File

@ -72,14 +72,15 @@ export type BodyOptions<T> = { [key in StringsOnly<keyof T>]: yargs.Options };
// eslint-disable-next-line max-params
export async function parseBody<
ArgT,
OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>
OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>,
ArgVT = ArgT & { _: Array<string> }
>(
msg: string,
command: Command,
prefix: string,
options: OptionT,
positionals: Array<[StringsOnly<keyof ArgT>, PositionalOptions]> = []
): Promise<[string, ArgT]> {
): Promise<[string, ArgVT]> {
const body = removeCommandName(msg, command, prefix);
// eslint-disable-next-line @typescript-eslint/ban-types
let parser: yargs.Argv<yargs.Omit<{}, keyof OptionT>> = yargs()
@ -93,7 +94,7 @@ export async function parseBody<
const args = await parser.parse(body);
return [args._.join(' '), args as unknown as ArgT]; // TODO: hax, the compiler says there is no overlap
return [args._.join(' '), args as unknown as ArgVT]; // TODO: hax, the compiler says there is no overlap
}
/**