start differentiating between channels, listen for correct exit signal
This commit is contained in:
parent
8d7b3d998f
commit
d97bd8fb61
12
src/args.ts
12
src/args.ts
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue