Skip to content

Commit

Permalink
additional redis cache (related #296)
Browse files Browse the repository at this point in the history
  • Loading branch information
Larsundso committed Jan 12, 2025
1 parent 50ff461 commit 09bbe50
Show file tree
Hide file tree
Showing 40 changed files with 2,601 additions and 9 deletions.
59 changes: 59 additions & 0 deletions src/BaseClient/Bot/Cache/automod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { APIAutoModerationRule } from 'discord.js';
import type Redis from 'ioredis';
import Cache from './base.js';

export type RAutomod = APIAutoModerationRule;

export const RAutomodKeys = [
'id',
'guild_id',
'name',
'creator_id',
'event_type',
'trigger_type',
'trigger_metadata',
'actions',
'enabled',
'exempt_roles',
'exempt_channels',
] as const;

export default class AutomodCache extends Cache<APIAutoModerationRule> {
public keys = RAutomodKeys;

constructor(prefix: string, redis: Redis) {
super(`${prefix}:automod`, redis);
}

key() {
return this.prefix;
}

async set(data: APIAutoModerationRule) {
const rData = this.apiToR(data);
if (!rData) return false;

await this.redis.set(`${this.key()}:${rData.guild_id}:${rData.id}`, JSON.stringify(rData));

return true;
}

get(id: string) {
return this.redis.get(`${this.key()}:${id}`).then((data) => this.stringToData(data));
}

del(id: string): Promise<number> {
return this.redis
.keys(`${this.key()}:${id}`)
.then((keys) => (keys.length ? this.redis.del(keys) : 0));
}

apiToR(data: APIAutoModerationRule) {
const keysNotToCache = Object.keys(data).filter(
(key): key is keyof typeof data => !this.keys.includes(key),
);

keysNotToCache.forEach((k) => delete data[k]);
return structuredClone(data) as RAutomod;
}
}
50 changes: 50 additions & 0 deletions src/BaseClient/Bot/Cache/ban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { APIBan } from 'discord.js';
import type Redis from 'ioredis';
import Cache from './base.js';

export type RBan = Omit<APIBan, 'user'> & { user_id: string; guild_id: string };

export const RBanKeys = ['reason', 'user_id', 'guild_id'] as const;

export default class BanCache extends Cache<APIBan> {
public keys = RBanKeys;

constructor(prefix: string, redis: Redis) {
super(`${prefix}:bans`, redis);
}

key() {
return this.prefix;
}

async set(data: APIBan, guildId: string) {
const rData = this.apiToR(data, guildId);
if (!rData) return false;

await this.redis.set(`${this.key()}:${rData.guild_id}:${rData.user_id}`, JSON.stringify(rData));

return true;
}

get(gId: string, uId: string) {
return this.redis.get(`${this.key()}:${gId}:${uId}`).then((data) => this.stringToData(data));
}

del(gId: string, uId: string): Promise<number> {
return this.redis.del(`${this.key()}:${gId}:${uId}`);
}

apiToR(data: APIBan, guildId: string) {
const keysNotToCache = Object.keys(data).filter(
(key): key is keyof typeof data => !this.keys.includes(key),
);

const rData = structuredClone(data) as unknown as RBan;
rData.guild_id = guildId;
rData.user_id = data.user.id;

keysNotToCache.forEach((k) => delete (rData as Record<string, unknown>)[k as string]);

return rData;
}
}
162 changes: 162 additions & 0 deletions src/BaseClient/Bot/Cache/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import type {
APIApplicationCommand,
APIApplicationCommandPermission,
APIAutoModerationRule,
APIBan,
APIEmoji,
APIGuild,
APIGuildChannel,
APIGuildIntegration,
APIGuildMember,
APIGuildScheduledEvent,
APIInvite,
APIMessage,
APIReaction,
APIRole,
APISoundboardSound,
APIStageInstance,
APISticker,
APIThreadChannel,
APIThreadMember,
APIUser,
APIVoiceState,
APIWebhook,
} from 'discord.js';
import type Redis from 'ioredis';
import type { RAutomod } from './automod';
import type { RBan } from './ban';
import type { RChannel, RChannelTypes } from './channel';
import type { RCommand } from './command';
import type { RCommandPermission } from './commandPermission';
import type { REmoji } from './emoji';
import type { REvent } from './event';
import type { RGuild } from './guild';
import type { RGuildCommand } from './guildCommand';
import type { RIntegration } from './integration';
import type { RInvite } from './invite';
import type { RMember } from './member';
import type { RMessage } from './message';
import type { RReaction } from './reaction';
import type { RRole } from './role';
import type { RSoundboardSound } from './soundboard';
import type { RStageInstance } from './stage';
import type { RSticker } from './sticker';
import type { RThread } from './thread';
import type { RThreadMember } from './threadMember';
import type { RUser } from './user';
import type { RVoiceState } from './voice';
import type { RWebhook } from './webhook';

type GuildBasedCommand<T extends boolean> = T extends true
? APIApplicationCommand & { guild_id: string }
: APIApplicationCommand;

type DeriveRFromAPI<T, K extends boolean> = T extends APIThreadChannel & {
guild_id: string;
member_id: string;
}
? RThread
: T extends APIGuildIntegration & {
user_id: string;
guild_id: string;
}
? RIntegration
: T extends APIApplicationCommand
? K extends true
? RGuildCommand
: RCommand
: T extends APIUser
? RUser
: T extends GuildBasedCommand<K>
? K extends true
? RGuildCommand
: RCommand
: T extends APIGuild
? RGuild
: T extends APISoundboardSound
? RSoundboardSound
: T extends APIGuildChannel<RChannelTypes>
? RChannel
: T extends APISticker
? RSticker
: T extends APIStageInstance
? RStageInstance
: T extends APIRole
? RRole
: T extends APIVoiceState
? RVoiceState
: T extends APIAutoModerationRule
? RAutomod
: T extends APIBan
? RBan
: T extends APIInvite
? RInvite
: T extends APIGuildMember
? RMember
: T extends APIGuildScheduledEvent
? REvent
: T extends APIWebhook
? RWebhook
: T extends APIEmoji
? REmoji
: T extends APIThreadChannel
? RThread
: T extends APIApplicationCommandPermission
? RCommandPermission
: T extends APIMessage
? RMessage
: T extends APIGuildIntegration
? RIntegration
: T extends APIReaction
? RReaction
: T extends APIThreadMember
? RThreadMember
: never;

export default abstract class Cache<
T extends
| APIUser
| APIGuild
| APISoundboardSound
| GuildBasedCommand<K>
| APISticker
| APIStageInstance
| APIRole
| APIVoiceState
| APIAutoModerationRule
| APIBan
| APIInvite
| APIGuildMember
| APIGuildScheduledEvent
| APIEmoji
| APIGuildChannel<RChannelTypes>
| APIThreadChannel
| APIApplicationCommandPermission
| APIMessage
| APIWebhook
| APIGuildIntegration
| APIReaction
| APIThreadMember,
K extends boolean = false,
> {
public prefix: string;
public redis: Redis;
abstract keys: ReadonlyArray<keyof DeriveRFromAPI<T, K>>;

constructor(prefix: string, redis: Redis) {
this.prefix = prefix;
this.redis = redis;
}

// eslint-disable-next-line class-methods-use-this
stringToData = (data: string | null) => (data ? (JSON.parse(data) as DeriveRFromAPI<T, K>) : null);

key(id?: string) {
return `${this.prefix}${id ? `:${id}` : '*'}`;
}

abstract set(...args: [T, string, string, string]): Promise<boolean>;
abstract get(...args: string[]): Promise<null | DeriveRFromAPI<T, K>>;
abstract del(...args: string[]): Promise<number>;
abstract apiToR(...args: [T, string, string, string]): DeriveRFromAPI<T, K> | false;
}
97 changes: 97 additions & 0 deletions src/BaseClient/Bot/Cache/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { APIGuildChannel, ChannelType } from 'discord.js';
import type Redis from 'ioredis';
import Cache from './base.js';

export type RChannelTypes =
| ChannelType.GuildAnnouncement
| ChannelType.GuildCategory
| ChannelType.GuildDirectory
| ChannelType.GuildForum
| ChannelType.GuildMedia
| ChannelType.GuildStageVoice
| ChannelType.GuildText
| ChannelType.GuildVoice;

export type RChannel = Omit<APIGuildChannel<RChannelTypes>, 'guild'> & {
guild_id: string;
};

export const RChannelKeys = [
'name',
'id',
'type',
'flags',
'guild_id',
'permission_overwrites',
'position',
'parent_id',
'nsfw',
'rate_limit_per_user',
'default_auto_archive_duration',
'default_thread_rate_limit_per_user',
'topic',
'bitrate',
'user_limit',
'rtc_region',
'video_quality_mode',
'last_pin_timestamp',
'available_tags',
'default_reaction_emoji',
'default_sort_order',
'default_forum_layout',
] as (keyof APIGuildChannel<ChannelType.GuildAnnouncement> &
keyof APIGuildChannel<ChannelType.GuildCategory> &
keyof APIGuildChannel<ChannelType.GuildDirectory> &
keyof APIGuildChannel<ChannelType.GuildForum> &
keyof APIGuildChannel<ChannelType.GuildMedia> &
keyof APIGuildChannel<ChannelType.GuildStageVoice> &
keyof APIGuildChannel<ChannelType.GuildText> &
keyof APIGuildChannel<ChannelType.GuildVoice>)[];

export default class ChannelCache extends Cache<
APIGuildChannel<RChannelTypes> & { guild_id: string }
> {
public keys = RChannelKeys;

constructor(prefix: string, redis: Redis) {
super(`${prefix}:channels`, redis);
}

key() {
return this.prefix;
}

async set(data: APIGuildChannel<RChannelTypes>) {
const rData = this.apiToR(data);
if (!rData) return false;

await this.redis.set(`${this.key()}:${data.guild_id}:${data.id}`, JSON.stringify(rData));

return true;
}

get(id: string) {
return this.redis.get(`${this.key()}:${id}`).then((data) => this.stringToData(data));
}

del(id: string): Promise<number> {
return this.redis
.keys(`${this.key()}:${id}`)
.then((keys) => (keys.length ? this.redis.del(keys) : 0));
}

apiToR(data: APIGuildChannel<RChannelTypes>) {
if (!data.guild_id) return false;

const keysNotToCache = Object.keys(data).filter(
(key): key is keyof typeof data => !this.keys.includes(key),
);

const rData = structuredClone(data) as unknown as RChannel;
rData.guild_id = data.guild_id;

keysNotToCache.forEach((k) => delete (rData as Record<string, unknown>)[k as string]);

return rData;
}
}
Loading

0 comments on commit 09bbe50

Please sign in to comment.