Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
Implement mediator pattern and WIP cache
Browse files Browse the repository at this point in the history
  • Loading branch information
aronson committed Oct 27, 2023
1 parent 18a62c2 commit b9cab2f
Show file tree
Hide file tree
Showing 10 changed files with 852 additions and 701 deletions.
667 changes: 28 additions & 639 deletions lib/bot.ts

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions lib/cache/asyncCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TTL } from '../deps.ts';

export abstract class AsyncCache<T> {
ttl: TTL<T>;

constructor(ttlMilliseconds: number) {
this.ttl = new TTL<T>(ttlMilliseconds);
}

protected abstract fetch(id: string): Promise<T>;

async get(id: string) {
let result = this.ttl.get(id);
if (result) return result;
result = await this.fetch(id);
this.ttl.set(id, result);
return result;
}

set(id: string, val: T) {
this.ttl.set(id, val);
}
}
15 changes: 15 additions & 0 deletions lib/cache/guildMemberCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AsyncCache } from './asyncCache.ts';
import { Guild, GuildMember } from '../deps.ts';

const TTL = 30_000;

export class GuildMemberCache extends AsyncCache<GuildMember> {
guild: Guild;
constructor(guild: Guild) {
super(TTL);
this.guild = guild;
}
async fetch(id: string): Promise<GuildMember> {
return await this.guild.members.fetch(id);
}
}
16 changes: 16 additions & 0 deletions lib/cache/memberRoleCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Role } from '../deps.ts';
import { AsyncCache } from './asyncCache.ts';
import { GuildMemberCache } from './guildMemberCache.ts';

const TTL = 30_000;
export class MemberRoleCache extends AsyncCache<Role[]> {
memberCache: GuildMemberCache;
constructor(memberCache: GuildMemberCache) {
super(TTL);
this.memberCache = memberCache;
}
async fetch(id: string): Promise<Role[]> {
const member = await this.memberCache.get(id);
return await member.roles.array();
}
}
2 changes: 2 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type IgnoreUsers = {
irc?: string[];
discord?: string[];
discordIds?: string[];
roles?: string[];
};

export type GameLogConfig = {
Expand Down Expand Up @@ -78,6 +79,7 @@ export const IgnoreUsersSchema = z.object({
irc: z.array(z.string()).optional(),
discord: z.array(z.string()).optional(),
discordIds: z.array(z.string()).optional(),
roles: z.array(z.string()).optional(),
});

export const GameLogConfigSchema = z.object({
Expand Down
8 changes: 7 additions & 1 deletion lib/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export {
GatewayIntents,
Guild,
GuildTextChannel,
Member as GuildMember,
Message,
Role,
User,
Webhook,
} from 'https://raw.githubusercontent.com/harmonyland/harmony/main/mod.ts';
Expand All @@ -38,7 +40,11 @@ export { parse as parseJSONC } from 'https://deno.land/std@0.203.0/jsonc/mod.ts'
import Dlog from 'https://deno.land/x/dlog2@2.0/classic.ts';
export { Dlog };
// PluralKit support
export { APIError, Member, PKAPI } from 'https://raw.githubusercontent.com/aronson/pkapi.ts/main/lib/mod.ts';
export {
APIError,
Member as PKMember,
PKAPI,
} from 'https://raw.githubusercontent.com/aronson/pkapi.ts/main/lib/mod.ts';
// Queue
export { Queue } from 'https://deno.land/x/queue@1.2.0/mod.ts';
// Time to Live cache
Expand Down
88 changes: 48 additions & 40 deletions lib/discordClient.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import Bot from './bot.ts';
import { escapeMarkdown } from './helpers.ts';
import { Command, CommandClient, CommandContext, event, GatewayIntents, Message } from './deps.ts';
import { ChannelMapper } from './channelMapping.ts';
import { Dictionary, escapeMarkdown } from './helpers.ts';
import { Command, CommandClient, CommandContext, Dlog, event, GatewayIntents, Message } from './deps.ts';
import { DEBUG, VERBOSE } from './env.ts';

class Names extends Command {
name = 'names';
private bot: Bot;
private channelMapping?: ChannelMapper;
private channelUsers: Dictionary<string[]>;
private logger: Dlog;

constructor(bot: Bot) {
constructor(channelUsers: Dictionary<Array<string>>, logger: Dlog) {
super();
this.bot = bot;
this.channelUsers = channelUsers;
this.logger = logger;
this.execute = this.execute.bind(this);
}

bindMap(map: ChannelMapper) {
this.channelMapping = map;
}

async execute(ctx: CommandContext): Promise<void> {
const ircChannel = this.bot?.channelMapping?.discordIdToMapping.get(ctx.channel.id)?.ircChannel;
const ircChannel = this.channelMapping?.discordIdToMapping.get(ctx.channel.id)?.ircChannel;
// return early if message was in channel we don't post to
if (!ircChannel) return;
const users = this.bot?.channelUsers[ircChannel];
const users = this.channelUsers[ircChannel];
if (users && users.length > 0) {
const ircNamesArr = new Array(...users);
await ctx.message.reply(
Expand All @@ -26,16 +35,23 @@ class Names extends Command {
}`,
);
} else {
this.bot.logger.warn(
this.logger.warn(
`No channelUsers found for ${ircChannel} when /names requested`,
);
}
}
}

export class DiscordClient extends CommandClient {
private bot: Bot;
constructor(bot: Bot) {
private logger: Dlog;
private sendMessageUpdates: boolean;
private names: Names;
constructor(
discordToken: string,
channelUsers: Dictionary<string[]>,
logger: Dlog,
sendMessageUpdates: boolean,
) {
super({
prefix: '/',
caseSensitive: false,
Expand All @@ -45,57 +61,49 @@ export class DiscordClient extends CommandClient {
GatewayIntents.GUILD_MESSAGES,
GatewayIntents.MESSAGE_CONTENT,
],
token: bot.config.discordToken,
token: discordToken,
});
this.bot = bot;
this.logger = logger;
this.sendMessageUpdates = sendMessageUpdates;
// Reconnect event has to be hooked manually due to naming conflict
this.on('reconnect', (shardId) => this.bot?.logger.info(`Reconnected to Discord (shard ID ${shardId})`));
this.commands.add(new Names(bot));
this.on('reconnect', (shardId) => logger.info(`Reconnected to Discord (shard ID ${shardId})`));
this.names = new Names(channelUsers, logger);
this.commands.add(this.names);
}

bindNotify(notify: (m: Message, b: boolean) => Promise<void>, mapper: ChannelMapper) {
this.on('messageCreate', async (ev) => await notify(ev, false));
this.on('messageUpdate', async (ev) => {
if (!this.sendMessageUpdates) return;
await notify(ev, true);
});
this.names.bindMap(mapper);
}

@event()
ready(): void {
this.bot.logger.done('Connected to Discord');
this.logger.done('Connected to Discord');
}

@event()
error(error: Error): void {
this.bot.logger.error('Received error event from Discord');
this.logger.error('Received error event from Discord');
console.log(error);
}

@event()
async messageCreate(message: Message): Promise<void> {
if (message.content.trim() === '/names') return;
if (!message.channel.isGuildText()) return;
// return early if message was in channel we don't post to
if (!(this.bot.channelMapping?.discordIdToMapping.get(message.channel.id))) {
return;
}
const ircChannel = this.bot?.channelMapping?.discordIdToMapping.get(message.channel.id)?.ircChannel;
if (!ircChannel) return;
await this.bot.sendToIRC(message);
}

@event()
async messageUpdate(_: Message, message: Message): Promise<void> {
if (!this.bot.config.sendMessageUpdates) return;
await this.bot.sendToIRC(message, true);
}

@event()
debug(message: string): void {
if (!this.bot.verbose && containsIgnoredMessage(message)) {
if (!VERBOSE && containsIgnoredMessage(message)) {
return;
}
if (!this.bot.debug) return;
this.bot.logger.debug(
if (!DEBUG) return;
this.logger.debug(
`Received debug event from Discord: ${JSON.stringify(message, null, 2)}`,
);
}
}

const ignoreMessages = [/Heartbeat ack/, /heartbeat sent/];
const ignoreMessages = [/Heartbeat ack/, /heartbeat sent/, /Shard/];

function containsIgnoredMessage(str: string): boolean {
return ignoreMessages.some((regex) => regex.test(str));
Expand Down
4 changes: 4 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const debug = 'DEBUG';
const verbose = 'VERBOSE';
export const DEBUG = (Deno.env.get(debug) ?? Deno.env.get(verbose) ?? 'false').toLowerCase() === 'true';
export const VERBOSE = (Deno.env.get('VERBOSE') ?? 'false').toLowerCase() === 'true';
44 changes: 23 additions & 21 deletions lib/ircClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Mediator } from './mediator.ts';
import { ChannelMapper, ChannelMapping } from './channelMapping.ts';
import Bot from './bot.ts';
import {
AnyRawCommand,
ClientError,
ClientOptions,
CtcpActionEvent,
Dlog,
InviteEvent,
IrcClient,
Expand All @@ -13,7 +13,6 @@ import {
NicklistEvent,
NoticeEvent,
PartEvent,
PrivmsgEvent,
QuitEvent,
RegisterEvent,
RemoteAddr,
Expand All @@ -32,8 +31,7 @@ const Event = (name: string) => Reflect.metadata('event', name);
export class CustomIrcClient extends IrcClient {
channelUsers: Dictionary<string[]>;
channelMapping: ChannelMapper;
sendToDiscord: (author: string, ircChannel: string, text: string) => Promise<void>;
sendExactToDiscord: (channel: string, text: string) => Promise<void>;
sendExactToDiscord: (channel: string, message: string) => Promise<void>;
exiting: () => boolean;
botNick: string;
debug: boolean;
Expand All @@ -50,12 +48,11 @@ export class CustomIrcClient extends IrcClient {
this.debug = bot.debug;
this.autoSendCommands = bot.config.autoSendCommands;
this.channelMapping = bot.channelMapping;
this.sendToDiscord = bot.sendToDiscord.bind(bot);
this.sendExactToDiscord = bot.sendExactToDiscord.bind(bot);
this.ircStatusNotices = bot.config.ircStatusNotices;
this.announceSelfJoin = bot.config.announceSelfJoin;
this.exiting = () => bot.exiting;
this.bindEvents();
this.sendExactToDiscord = async () => {};
}
// Bind event handlers to base client through Reflect metadata and bind each handler to this instance
bindEvents() {
Expand Down Expand Up @@ -122,13 +119,26 @@ export class CustomIrcClient extends IrcClient {
`Received error event from IRC\n${JSON.stringify(error, null, 2)}`,
);
}
@Event('privmsg:channel')
async onPrivMessage(event: PrivmsgEvent) {
await this.sendToDiscord(
event.source?.name ?? '',
event.params.target,
event.params.text,
);
bindNotify(
fn: (author: string, channel: string, message: string, raw: boolean) => Promise<void>,
mediator: Mediator,
) {
const raw = false;
this.on('privmsg:channel', async (event) =>
await fn(
event.source?.name ?? '',
event.params.target,
event.params.text,
raw,
));
this.on('ctcp_action', async (event) =>
await fn(
event.source?.name ?? '',
event.params.target,
`_${event.params.text}_`,
raw,
));
this.sendExactToDiscord = mediator.sendExactToDiscord;
}
@Event('notice')
onNotice(event: NoticeEvent) {
Expand Down Expand Up @@ -261,14 +271,6 @@ export class CustomIrcClient extends IrcClient {
const channel = channelName.toLowerCase();
this.channelUsers[channel] = nicks.map((n) => n.nick);
}
@Event('ctcp_action')
async onAction(event: CtcpActionEvent) {
await this.sendToDiscord(
event.source?.name ?? '',
event.params.target,
`_${event.params.text}_`,
);
}
@Event('invite')
onInvite(event: InviteEvent) {
const channel = event.params.channel;
Expand Down
Loading

0 comments on commit b9cab2f

Please sign in to comment.