add source bot to command context, add user lookup to bot, allow giving karma to others
This commit is contained in:
parent
0e7b153797
commit
3e483d4a31
8
Makefile
8
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
|
13
src/main.ts
13
src/main.ts
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue