1
0
Fork 0

start differentiating between channels, listen for correct exit signal

This commit is contained in:
Sean Sube 2022-12-26 23:58:30 -06:00
parent 8d7b3d998f
commit d97bd8fb61
13 changed files with 121 additions and 61 deletions

View File

@ -7,6 +7,8 @@ import yargs from 'yargs';
*/
export interface ParsedArgs {
bots: Array<string>;
channelId: string;
channelTopic: string;
commandAdmins: Array<string>;
commandPrefix: string;
data: string;
@ -39,6 +41,16 @@ export async function parseArgs(argv: Array<string>): Promise<ParsedArgs> {
string: true,
type: 'array',
},
channelId: {
default: '',
desc: 'channel identifier',
type: 'string',
},
channelTopic: {
default: 'players online: %s',
desc: 'channel topic template',
type: 'string',
},
commandAdmins: {
default: [] as Array<string>,
desc: 'admin command users',

View File

@ -1,4 +1,4 @@
import { doesExist, mustExist } from '@apextoaster/js-utils';
import { doesExist, mustDefault, mustExist, NotImplementedError } from '@apextoaster/js-utils';
import {
ChannelType,
Client,
@ -7,7 +7,7 @@ import {
GatewayIntentBits,
Message as DiscordMessage,
Partials,
TextChannel,
TextBasedChannel,
} from 'discord.js';
import { CommandContext } from '../command/index.js';
@ -20,11 +20,6 @@ export async function onMessage(context: CommandContext, client: Client<true>, d
const { author, content } = discordMessage;
const name = authorName(author);
logger.debug({
text: content,
user: name,
}, 'message created');
if (name === mustExist(client.user).tag) {
logger.debug('own message, ignoring');
return;
@ -33,6 +28,8 @@ export async function onMessage(context: CommandContext, client: Client<true>, d
const matchingCommands = matchCommands(content, commands, args.commandPrefix);
const message = await messageFromDiscord(client, discordMessage);
logger.debug({ message }, 'message received');
for (const command of matchingCommands) {
logger.debug({
command: command.name,
@ -54,13 +51,30 @@ export async function onMessage(context: CommandContext, client: Client<true>, d
}
}
async function channelFromDiscord(client: Client, channel: TextChannel): Promise<Channel> {
return {
name: channel.id,
async edit(options) {
await channel.edit(options);
}
};
async function channelFromDiscord(_client: Client, channel: TextBasedChannel): Promise<Channel> {
switch (channel.type) {
case ChannelType.GuildAnnouncement:
case ChannelType.GuildText:
return {
name: channel.name,
server: channel.guild.name,
topic: mustDefault(channel.topic, ''),
type: 'text',
async edit(options) {
const edited = await channel.edit(options);
return channelFromDiscord(_client, edited);
}
};
case ChannelType.DM:
return {
name: channel.id,
type: 'dm',
};
case ChannelType.PrivateThread:
case ChannelType.PublicThread:
default:
throw new Error('invalid channel type');
}
}
async function messageFromDiscord(client: Client, message: DiscordMessage): Promise<Message> {
@ -83,7 +97,7 @@ async function messageFromDiscord(client: Client, message: DiscordMessage): Prom
async function getChannel(client: Client, name: string): Promise<Channel> {
const channel = await client.channels.fetch(name);
if (doesExist(channel)) {
if (channel.type === ChannelType.GuildText) {
if (channel.type === ChannelType.GuildText || channel.type === ChannelType.DM) {
return channelFromDiscord(client, channel);
} else {
throw new Error('invalid channel type');

View File

@ -17,15 +17,28 @@ export interface Author {
username: string;
}
export interface Channel {
export type ChannelType = 'dm' | 'text';
export interface BaseChannel {
name: string;
type: ChannelType;
}
export interface TextChannel extends BaseChannel {
server: string;
topic: string;
edit(options: {
name?: string;
topic?: string;
}): Promise<void>;
}): Promise<Channel>;
}
// anything unique?
export type DirectChannel = BaseChannel;
export type Channel = DirectChannel | TextChannel;
/**
* Text message with ability to reply.
*

View File

@ -71,9 +71,7 @@ export async function readlineConnect(botContext: BotContext, readlineFactory: t
const channel: Channel = {
name: 'readline',
async edit(options) {
logger.info({ options }, 'editing channel');
},
type: 'dm',
};
const bot: Bot = {
@ -92,6 +90,11 @@ export async function readlineConnect(botContext: BotContext, readlineFactory: t
},
};
rl.on('close', () => {
logger.info('readline bot closed');
// process.kill(process.pid, 'SIGINT');
});
rl.on('line', (line) => catchAndLog(onLine({
...botContext,
bot,

View File

@ -1,23 +1,50 @@
import { mustExist } from '@apextoaster/js-utils';
import { gracefulShutdown, scheduleJob } from 'node-schedule';
import { format as sprintf } from 'node:util';
import { Logger } from 'noicejs';
import { ParsedArgs } from '../args.js';
import { Bot } from '../bot/index.js';
import { DataLayer } from '../data/index.js';
import { RconClient } from '../utils/rcon.js';
export interface Job {
destroy(): Promise<void>;
}
export async function startCrons(args: ParsedArgs, logger: Logger, bot: Bot): Promise<Job> {
const job = scheduleJob('test', '*/1 * * * *', async () => {
logger.info('test job');
export interface JobContext {
args: ParsedArgs;
bot: Bot;
data: DataLayer;
logger: Logger;
rcon: RconClient;
}
const channel = mustExist(await bot.getChannel('foo'));
await channel.edit({
topic: 'test',
});
});
export async function playerCountJob(context: JobContext): Promise<void> {
const { args, bot, logger, rcon } = context;
logger.info('player count job');
const players = await rcon.send('listplayers');
const count = players.trim().split('\n').length - 1;
logger.debug({ players, count }, 'player count cron');
// TODO: use `data` to store value/check for changes
const channel = mustExist(await bot.getChannel(args.channelId));
const topic = sprintf(args.channelTopic, count);
logger.info({ channel, topic }, 'updating channel topic');
// await channel.edit({
// topic: `${topic}, players online: ${count}`,
// });
}
export async function startCrons(context: JobContext): Promise<Job> {
const { logger } = context;
const job = scheduleJob('test', '*/1 * * * *', () => playerCountJob(context));
logger.debug({ next: job.nextInvocation() }, 'next cron');
return {
destroy() {

View File

@ -1,4 +1,4 @@
import { signal, SIGNAL_STOP } from '@apextoaster/js-utils';
import { signal, SIGNAL_RESET } from '@apextoaster/js-utils';
import { createLogger, DEBUG } from 'bunyan';
import { APP_NAME, parseArgs } from './args.js';
@ -69,9 +69,9 @@ export async function main(argv: Array<string>): Promise<ExitCode> {
}
logger.debug('starting cron jobs');
const cron = await startCrons(args, logger, bots[0]);
const cron = await startCrons(args, logger, bots[0], rcon);
await signal(SIGNAL_STOP);
await signal(SIGNAL_RESET);
logger.info('stopping cron jobs');
await cron.destroy();

View File

@ -52,6 +52,9 @@ describe('discord bot', () => {
});
discord.channels = createStubInstance(ChannelManager);
discord.channels.fetch = stub().resolves({
guild: {
name: '',
},
id: 'test',
type: ChannelType.GuildText,
});
@ -97,6 +100,9 @@ describe('discord bot', () => {
});
discord.channels = createStubInstance(ChannelManager);
discord.channels.fetch = stub().resolves({
guild: {
name: '',
},
id: 'test',
type: ChannelType.GuildText,
});

View File

@ -9,7 +9,7 @@ import { BotContext, Channel } from '../../src/bot/index.js';
import { onLine, readlineConnect } from '../../src/bot/readline.js';
import { Command, CommandContext } from '../../src/command/index.js';
import { MemoryDataLayer } from '../../src/data/memory.js';
import { mockBot, mockRcon, REQUIRED_ARGS } from '../helpers.js';
import { mockBot, mockChannel, mockRcon, REQUIRED_ARGS } from '../helpers.js';
describe('readline bot', () => {
it('should close the underlying readline instance', async () => {
@ -44,10 +44,7 @@ describe('readline bot', () => {
...REQUIRED_ARGS,
'--commandPrefix="!"',
]);
const channel: Channel = {
name: 'test',
edit: stub().resolves(),
};
const channel = mockChannel();
const command: Command = {
name: 'test',
desc: 'test',
@ -77,10 +74,7 @@ describe('readline bot', () => {
...REQUIRED_ARGS,
'--commandPrefix="!"',
]);
const channel: Channel = {
name: 'test',
edit: stub().resolves(),
};
const channel = mockChannel();
const command: Command = {
name: 'test',
desc: '',

View File

@ -7,7 +7,7 @@ import { Author, Message } from '../../../src/bot/index.js';
import { rcon } from '../../../src/command/admin/rcon.js';
import { CommandContext } from '../../../src/command/index.js';
import { MemoryDataLayer } from '../../../src/data/memory.js';
import { mockRcon, REQUIRED_ARGS } from '../../helpers.js';
import { mockChannel, mockRcon, REQUIRED_ARGS } from '../../helpers.js';
describe('rcon command', () => {
it('should execute the given command', async () => {
@ -36,10 +36,7 @@ describe('rcon command', () => {
const replyMock = mock().named('Message.reply').resolves();
const message: Message = {
author,
channel: {
name: 'test',
edit: mock().resolves(),
},
channel: mockChannel(),
content: '',
reply: replyMock,
};

View File

@ -7,7 +7,7 @@ import { Author, Message } from '../../../src/bot/index.js';
import { rconSQL } from '../../../src/command/admin/rcon-sql.js';
import { CommandContext } from '../../../src/command/index.js';
import { MemoryDataLayer } from '../../../src/data/memory.js';
import { mockRcon, REQUIRED_ARGS } from '../../helpers.js';
import { mockChannel, mockRcon, REQUIRED_ARGS } from '../../helpers.js';
describe('rcon SQL command', () => {
it('should execute the given query', async () => {
@ -36,10 +36,7 @@ describe('rcon SQL command', () => {
const replyMock = mock().named('Message.reply').resolves();
const message: Message = {
author,
channel: {
name: 'test',
edit: mock().resolves(),
},
channel: mockChannel(),
content: '',
reply: replyMock,
};

View File

@ -7,7 +7,7 @@ import { Author, Message } from '../../../src/bot/index.js';
import { conanOnline } from '../../../src/command/conan/online.js';
import { CommandContext } from '../../../src/command/index.js';
import { MemoryDataLayer } from '../../../src/data/memory.js';
import { mockRcon, REQUIRED_ARGS } from '../../helpers.js';
import { mockChannel, mockRcon, REQUIRED_ARGS } from '../../helpers.js';
describe('conan online command', () => {
it('should invoke rcon listplayers', async () => {
@ -36,10 +36,7 @@ describe('conan online command', () => {
const replyMock = mock().named('Message.reply').resolves();
const message: Message = {
author,
channel: {
name: 'test',
edit: mock().resolves(),
},
channel: mockChannel(),
content: '',
reply: replyMock,
};

View File

@ -7,7 +7,7 @@ import { Author, Message } from '../../../src/bot/index.js';
import { conanRecent } from '../../../src/command/conan/recent.js';
import { CommandContext } from '../../../src/command/index.js';
import { MemoryDataLayer } from '../../../src/data/memory.js';
import { mockRcon, REQUIRED_ARGS } from '../../helpers.js';
import { mockChannel, mockRcon, REQUIRED_ARGS } from '../../helpers.js';
describe('conan recent command', () => {
it('should invoke rcon sql', async () => {
@ -39,10 +39,7 @@ describe('conan recent command', () => {
const replyMock = mock().named('Message.reply').resolves();
const message: Message = {
author,
channel: {
name: 'test',
edit: mock().resolves(),
},
channel: mockChannel(),
content: '',
reply: replyMock,
};

View File

@ -42,8 +42,11 @@ export function mockAuthor(username = 'test', discriminator = '0000'): Author {
export function mockChannel(name = 'test', edit = stub().resolves()): Channel {
return {
name,
edit,
name,
server: '',
topic: '',
type: 'text',
};
}