aboutsummaryrefslogtreecommitdiff
path: root/lib/models
diff options
context:
space:
mode:
Diffstat (limited to 'lib/models')
-rw-r--r--lib/models/BaseModel.ts13
-rw-r--r--lib/models/instance/ActivePunishment.ts94
-rw-r--r--lib/models/instance/Guild.ts431
-rw-r--r--lib/models/instance/Highlight.ts81
-rw-r--r--lib/models/instance/Level.ts70
-rw-r--r--lib/models/instance/ModLog.ts127
-rw-r--r--lib/models/instance/Reminder.ts84
-rw-r--r--lib/models/instance/StickyRole.ts58
-rw-r--r--lib/models/shared/Global.ts67
-rw-r--r--lib/models/shared/GuildCount.ts38
-rw-r--r--lib/models/shared/MemberCount.ts37
-rw-r--r--lib/models/shared/Shared.ts84
-rw-r--r--lib/models/shared/Stat.ts72
13 files changed, 1256 insertions, 0 deletions
diff --git a/lib/models/BaseModel.ts b/lib/models/BaseModel.ts
new file mode 100644
index 0000000..8fba5e5
--- /dev/null
+++ b/lib/models/BaseModel.ts
@@ -0,0 +1,13 @@
+import { Model } from 'sequelize';
+
+export abstract class BaseModel<A, B> extends Model<A, B> {
+ /**
+ * The date when the row was created.
+ */
+ public declare readonly createdAt: Date;
+
+ /**
+ * The date when the row was last updated.
+ */
+ public declare readonly updatedAt: Date;
+}
diff --git a/lib/models/instance/ActivePunishment.ts b/lib/models/instance/ActivePunishment.ts
new file mode 100644
index 0000000..38012ca
--- /dev/null
+++ b/lib/models/instance/ActivePunishment.ts
@@ -0,0 +1,94 @@
+import { type Snowflake } from 'discord.js';
+import { nanoid } from 'nanoid';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export enum ActivePunishmentType {
+ BAN = 'BAN',
+ MUTE = 'MUTE',
+ ROLE = 'ROLE',
+ BLOCK = 'BLOCK'
+}
+
+export interface ActivePunishmentModel {
+ id: string;
+ type: ActivePunishmentType;
+ user: Snowflake;
+ guild: Snowflake;
+ extraInfo: Snowflake;
+ expires: Date | null;
+ modlog: string;
+}
+
+export interface ActivePunishmentModelCreationAttributes {
+ id?: string;
+ type: ActivePunishmentType;
+ user: Snowflake;
+ guild: Snowflake;
+ extraInfo?: Snowflake;
+ expires?: Date;
+ modlog: string;
+}
+
+/**
+ * Keeps track of active punishments so they can be removed later.
+ */
+export class ActivePunishment
+ extends BaseModel<ActivePunishmentModel, ActivePunishmentModelCreationAttributes>
+ implements ActivePunishmentModel
+{
+ /**
+ * The ID of this punishment (no real use just for a primary key)
+ */
+ public declare id: string;
+
+ /**
+ * The type of punishment.
+ */
+ public declare type: ActivePunishmentType;
+
+ /**
+ * The user who is punished.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The guild they are punished in.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * Additional info about the punishment if applicable. The channel id for channel blocks and role for punishment roles.
+ */
+ public declare extraInfo: Snowflake;
+
+ /**
+ * The date when this punishment expires (optional).
+ */
+ public declare expires: Date | null;
+
+ /**
+ * The reference to the modlog entry.
+ */
+ public declare modlog: string;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ ActivePunishment.init(
+ {
+ id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid },
+ type: { type: DataTypes.STRING, allowNull: false },
+ user: { type: DataTypes.STRING, allowNull: false },
+ guild: { type: DataTypes.STRING, allowNull: false, references: { model: 'Guilds', key: 'id' } },
+ extraInfo: { type: DataTypes.STRING, allowNull: true },
+ expires: { type: DataTypes.DATE, allowNull: true },
+ modlog: { type: DataTypes.STRING, allowNull: true, references: { model: 'ModLogs', key: 'id' } }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts
new file mode 100644
index 0000000..f258d48
--- /dev/null
+++ b/lib/models/instance/Guild.ts
@@ -0,0 +1,431 @@
+import { ChannelType, Constants, type Snowflake } from 'discord.js';
+import { type Sequelize } from 'sequelize';
+import { BadWordDetails } from '../../automod/AutomodShared.js';
+import { type BushClient } from '../../extensions/discord-akairo/BushClient.js';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface GuildModel {
+ id: Snowflake;
+ prefix: string;
+ autoPublishChannels: Snowflake[];
+ blacklistedChannels: Snowflake[];
+ blacklistedUsers: Snowflake[];
+ welcomeChannel: Snowflake | null;
+ muteRole: Snowflake | null;
+ punishmentEnding: string | null;
+ disabledCommands: string[];
+ lockdownChannels: Snowflake[];
+ autoModPhases: BadWordDetails[];
+ enabledFeatures: GuildFeatures[];
+ joinRoles: Snowflake[];
+ logChannels: LogChannelDB;
+ bypassChannelBlacklist: Snowflake[];
+ noXpChannels: Snowflake[];
+ levelRoles: { [level: number]: Snowflake };
+ levelUpChannel: Snowflake | null;
+}
+
+export interface GuildModelCreationAttributes {
+ id: Snowflake;
+ prefix?: string;
+ autoPublishChannels?: Snowflake[];
+ blacklistedChannels?: Snowflake[];
+ blacklistedUsers?: Snowflake[];
+ welcomeChannel?: Snowflake;
+ muteRole?: Snowflake;
+ punishmentEnding?: string;
+ disabledCommands?: string[];
+ lockdownChannels?: Snowflake[];
+ autoModPhases?: BadWordDetails[];
+ enabledFeatures?: GuildFeatures[];
+ joinRoles?: Snowflake[];
+ logChannels?: LogChannelDB;
+ bypassChannelBlacklist?: Snowflake[];
+ noXpChannels?: Snowflake[];
+ levelRoles?: { [level: number]: Snowflake };
+ levelUpChannel?: Snowflake;
+}
+
+/**
+ * Settings for a guild.
+ */
+export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> implements GuildModel {
+ /**
+ * The ID of the guild
+ */
+ public declare id: Snowflake;
+
+ /**
+ * The bot's prefix for the guild
+ */
+ public declare prefix: string;
+
+ /**
+ * Channels that will have their messages automatically published
+ */
+ public declare autoPublishChannels: Snowflake[];
+
+ /**
+ * Channels where the bot won't respond in.
+ */
+ public declare blacklistedChannels: Snowflake[];
+
+ /**
+ * Users that the bot ignores in this guild
+ */
+ public declare blacklistedUsers: Snowflake[];
+
+ /**
+ * The channels where the welcome messages are sent
+ */
+ public declare welcomeChannel: Snowflake | null;
+
+ /**
+ * The role given out when muting someone
+ */
+ public declare muteRole: Snowflake | null;
+
+ /**
+ * The message that gets sent after someone gets a punishment dm
+ */
+ public declare punishmentEnding: string | null;
+
+ /**
+ * Guild specific disabled commands
+ */
+ public declare disabledCommands: string[];
+
+ /**
+ * Channels that should get locked down when the lockdown command gets used.
+ */
+ public declare lockdownChannels: Snowflake[];
+
+ /**
+ * Custom automod phases
+ */
+ public declare autoModPhases: BadWordDetails[];
+
+ /**
+ * The features enabled in a guild
+ */
+ public declare enabledFeatures: GuildFeatures[];
+
+ /**
+ * The roles to assign to a user if they are not assigned sticky roles
+ */
+ public declare joinRoles: Snowflake[];
+
+ /**
+ * The channels where logging messages will be sent.
+ */
+ public declare logChannels: LogChannelDB;
+
+ /**
+ * These users will be able to use commands in channels blacklisted
+ */
+ public declare bypassChannelBlacklist: Snowflake[];
+
+ /**
+ * Channels where users will not earn xp for leveling.
+ */
+ public declare noXpChannels: Snowflake[];
+
+ /**
+ * What roles get given to users when they reach certain levels.
+ */
+ public declare levelRoles: { [level: number]: Snowflake };
+
+ /**
+ * The channel to send level up messages in instead of last channel.
+ */
+ public declare levelUpChannel: Snowflake | null;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize, client: BushClient): void {
+ Guild.init(
+ {
+ id: { type: DataTypes.STRING, primaryKey: true },
+ prefix: { type: DataTypes.TEXT, allowNull: false, defaultValue: client.config.prefix },
+ autoPublishChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ welcomeChannel: { type: DataTypes.STRING, allowNull: true },
+ muteRole: { type: DataTypes.STRING, allowNull: true },
+ punishmentEnding: { type: DataTypes.TEXT, allowNull: true },
+ disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ lockdownChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ autoModPhases: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ enabledFeatures: {
+ type: DataTypes.JSONB,
+ allowNull: false,
+ defaultValue: Object.keys(guildFeaturesObj).filter(
+ (key) => guildFeaturesObj[key as keyof typeof guildFeaturesObj].default
+ )
+ },
+ joinRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ logChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} },
+ bypassChannelBlacklist: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ noXpChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ levelRoles: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} },
+ levelUpChannel: { type: DataTypes.STRING, allowNull: true }
+ },
+ { sequelize }
+ );
+ }
+}
+
+export type BaseGuildSetting = 'channel' | 'role' | 'user';
+export type GuildNoArraySetting = 'string' | 'custom' | BaseGuildSetting;
+export type GuildSettingType = GuildNoArraySetting | `${BaseGuildSetting}-array`;
+
+export interface GuildSetting {
+ name: string;
+ description: string;
+ type: GuildSettingType;
+ subType: ChannelType[] | undefined;
+ configurable: boolean;
+ replaceNullWith: () => string | null;
+}
+const asGuildSetting = <T>(et: { [K in keyof T]: PartialBy<GuildSetting, 'configurable' | 'subType' | 'replaceNullWith'> }) => {
+ for (const key in et) {
+ et[key].subType ??= undefined;
+ et[key].configurable ??= true;
+ et[key].replaceNullWith ??= () => null;
+ }
+ return et as { [K in keyof T]: GuildSetting };
+};
+
+const { default: config } = await import('../../../config/options.js');
+
+export const guildSettingsObj = asGuildSetting({
+ prefix: {
+ name: 'Prefix',
+ description: 'The phrase required to trigger text commands in this server.',
+ type: 'string',
+ replaceNullWith: () => config.prefix
+ },
+ autoPublishChannels: {
+ name: 'Auto Publish Channels',
+ description: 'Channels were every message is automatically published.',
+ type: 'channel-array',
+ subType: [ChannelType.GuildNews]
+ },
+ welcomeChannel: {
+ name: 'Welcome Channel',
+ description: 'The channel where the bot will send join and leave message.',
+ type: 'channel',
+ subType: [
+ ChannelType.GuildText,
+ ChannelType.GuildNews,
+ ChannelType.GuildNewsThread,
+ ChannelType.GuildPublicThread,
+ ChannelType.GuildPrivateThread
+ ]
+ },
+ muteRole: {
+ name: 'Mute Role',
+ description: 'The role assigned when muting someone.',
+ type: 'role'
+ },
+ punishmentEnding: {
+ name: 'Punishment Ending',
+ description: 'The message after punishment information to a user in a dm.',
+ type: 'string'
+ },
+ lockdownChannels: {
+ name: 'Lockdown Channels',
+ description: 'Channels that are locked down when a mass lockdown is specified.',
+ type: 'channel-array',
+ subType: [ChannelType.GuildText]
+ },
+ joinRoles: {
+ name: 'Join Roles',
+ description: 'Roles assigned to users on join who do not have sticky role information.',
+ type: 'role-array'
+ },
+ bypassChannelBlacklist: {
+ name: 'Bypass Channel Blacklist',
+ description: 'These users will be able to use commands in channels blacklisted.',
+ type: 'user-array'
+ },
+ logChannels: {
+ name: 'Log Channels',
+ description: 'The channel were logs are sent.',
+ type: 'custom',
+ subType: [ChannelType.GuildText],
+ configurable: false
+ },
+ autoModPhases: {
+ name: 'Automod Phases',
+ description: 'Custom phrases to be detected by automod.',
+ type: 'custom',
+ configurable: false
+ },
+ noXpChannels: {
+ name: 'No Xp Channels',
+ description: 'Channels where users will not earn xp for leveling.',
+ type: 'channel-array',
+ subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM)
+ },
+ levelRoles: {
+ name: 'Level Roles',
+ description: 'What roles get given to users when they reach certain levels.',
+ type: 'custom',
+ configurable: false
+ },
+ levelUpChannel: {
+ name: 'Level Up Channel',
+ description: 'The channel to send level up messages in instead of last channel.',
+ type: 'channel',
+ subType: Constants.TextBasedChannelTypes.filter((type) => type !== ChannelType.DM)
+ }
+});
+
+export type GuildSettings = keyof typeof guildSettingsObj;
+export const settingsArr = Object.keys(guildSettingsObj).filter(
+ (s) => guildSettingsObj[s as GuildSettings].configurable
+) as GuildSettings[];
+
+interface GuildFeature {
+ name: string;
+ description: string;
+ default: boolean;
+ hidden: boolean;
+}
+
+type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+
+const asGuildFeature = <T>(gf: { [K in keyof T]: PartialBy<GuildFeature, 'hidden' | 'default'> }): {
+ [K in keyof T]: GuildFeature;
+} => {
+ for (const key in gf) {
+ gf[key].hidden ??= false;
+ gf[key].default ??= false;
+ }
+ return gf as { [K in keyof T]: GuildFeature };
+};
+
+export const guildFeaturesObj = asGuildFeature({
+ automod: {
+ name: 'Automod',
+ description: 'Deletes offensive content as well as phishing links.'
+ },
+ excludeDefaultAutomod: {
+ name: 'Exclude Default Automod',
+ description: 'Opt out of using the default automod options.'
+ },
+ excludeAutomodScamLinks: {
+ name: 'Exclude Automod Scam Links',
+ description: 'Opt out of having automod delete scam links.'
+ },
+ delScamMentions: {
+ name: 'Delete Scam Mentions',
+ description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.'
+ },
+ automodPresence: {
+ name: 'Automod Presence',
+ description: 'Logs presence changes that trigger automod.',
+ hidden: true
+ },
+ automodMembers: {
+ name: 'Automod Members',
+ description: "Logs members' usernames and nicknames changes if they match automod."
+ },
+ blacklistedFile: {
+ name: 'Blacklisted File',
+ description: 'Automatically deletes malicious files.'
+ },
+ autoPublish: {
+ name: 'Auto Publish',
+ description: 'Publishes messages in configured announcement channels.'
+ },
+ // todo implement a better auto thread system
+ autoThread: {
+ name: 'Auto Thread',
+ description: 'Creates a new thread for messages in configured channels.',
+ hidden: true
+ },
+ perspectiveApi: {
+ name: 'Perspective API',
+ description: 'Use the Perspective API to detect toxicity.',
+ hidden: true
+ },
+ boosterMessageReact: {
+ name: 'Booster Message React',
+ description: 'Reacts to booster messages with the boost emoji.'
+ },
+ leveling: {
+ name: 'Leveling',
+ description: "Tracks users' messages and assigns them xp."
+ },
+ sendLevelUpMessages: {
+ name: 'Send Level Up Messages',
+ description: 'Send a message when a user levels up.',
+ default: true
+ },
+ stickyRoles: {
+ name: 'Sticky Roles',
+ description: 'Restores past roles to a user when they rejoin.'
+ },
+ reporting: {
+ name: 'Reporting',
+ description: 'Allow users to make reports.'
+ },
+ modsCanPunishMods: {
+ name: 'Mods Can Punish Mods',
+ description: 'Allow moderators to punish other moderators.'
+ },
+ logManualPunishments: {
+ name: 'Log Manual Punishments',
+ description: "Adds manual punishment to the user's modlogs and the logging channels.",
+ default: true
+ },
+ punishmentAppeals: {
+ name: 'Punishment Appeals',
+ description: 'Allow users to appeal their punishments and send the appeal to the configured channel.',
+ hidden: true
+ },
+ highlight: {
+ name: 'Highlight',
+ description: 'Allows the highlight command to be used.',
+ default: true
+ }
+});
+
+export const guildLogsObj = {
+ automod: {
+ description: 'Sends a message in this channel every time automod is activated.',
+ configurable: true
+ },
+ moderation: {
+ description: 'Sends a message in this channel every time a moderation action is performed.',
+ configurable: true
+ },
+ report: {
+ description: 'Logs user reports.',
+ configurable: true
+ },
+ error: {
+ description: 'Logs errors that occur with the bot.',
+ configurable: true
+ },
+ appeals: {
+ description: 'Where punishment appeals are sent.',
+ configurable: false
+ }
+};
+
+export type GuildLogType = keyof typeof guildLogsObj;
+export const guildLogsArr = Object.keys(guildLogsObj).filter(
+ (s) => guildLogsObj[s as GuildLogType].configurable
+) as GuildLogType[];
+type LogChannelDB = { [x in keyof typeof guildLogsObj]?: Snowflake };
+
+export type GuildFeatures = keyof typeof guildFeaturesObj;
+export const guildFeaturesArr: GuildFeatures[] = Object.keys(guildFeaturesObj).filter(
+ (f) => !guildFeaturesObj[f as keyof typeof guildFeaturesObj].hidden
+) as GuildFeatures[];
diff --git a/lib/models/instance/Highlight.ts b/lib/models/instance/Highlight.ts
new file mode 100644
index 0000000..5889fad
--- /dev/null
+++ b/lib/models/instance/Highlight.ts
@@ -0,0 +1,81 @@
+import { type Snowflake } from 'discord.js';
+import { nanoid } from 'nanoid';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface HighlightModel {
+ pk: string;
+ user: Snowflake;
+ guild: Snowflake;
+ words: HighlightWord[];
+ blacklistedChannels: Snowflake[];
+ blacklistedUsers: Snowflake[];
+}
+
+export interface HighLightCreationAttributes {
+ pk?: string;
+ user: Snowflake;
+ guild: Snowflake;
+ words?: HighlightWord[];
+ blacklistedChannels?: Snowflake[];
+ blacklistedUsers?: Snowflake[];
+}
+
+export interface HighlightWord {
+ word: string;
+ regex: boolean;
+}
+
+/**
+ * List of words that should cause the user to be notified for if found in the specified guild.
+ */
+export class Highlight extends BaseModel<HighlightModel, HighLightCreationAttributes> implements HighlightModel {
+ /**
+ * The primary key of the highlight.
+ */
+ public declare pk: string;
+
+ /**
+ * The user that the highlight is for.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The guild to look for highlights in.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * The words to look for.
+ */
+ public declare words: HighlightWord[];
+
+ /**
+ * Channels that the user choose to ignore highlights in.
+ */
+ public declare blacklistedChannels: Snowflake[];
+
+ /**
+ * Users that the user choose to ignore highlights from.
+ */
+ public declare blacklistedUsers: Snowflake[];
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Highlight.init(
+ {
+ pk: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid },
+ user: { type: DataTypes.STRING, allowNull: false },
+ guild: { type: DataTypes.STRING, allowNull: false },
+ words: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/instance/Level.ts b/lib/models/instance/Level.ts
new file mode 100644
index 0000000..d8d16f0
--- /dev/null
+++ b/lib/models/instance/Level.ts
@@ -0,0 +1,70 @@
+import { type Snowflake } from 'discord.js';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface LevelModel {
+ user: Snowflake;
+ guild: Snowflake;
+ xp: number;
+}
+
+export interface LevelModelCreationAttributes {
+ user: Snowflake;
+ guild: Snowflake;
+ xp?: number;
+}
+
+/**
+ * Leveling information for a user in a guild.
+ */
+export class Level extends BaseModel<LevelModel, LevelModelCreationAttributes> implements LevelModel {
+ /**
+ * The user's id.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The guild where the user is gaining xp.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * The user's xp.
+ */
+ public declare xp: number;
+
+ /**
+ * The user's level.
+ */
+ public get level(): number {
+ return Level.convertXpToLevel(this.xp);
+ }
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Level.init(
+ {
+ user: { type: DataTypes.STRING, allowNull: false },
+ guild: { type: DataTypes.STRING, allowNull: false },
+ xp: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }
+ },
+ { sequelize }
+ );
+ }
+
+ public static convertXpToLevel(xp: number): number {
+ return Math.floor((-25 + Math.sqrt(625 + 200 * xp)) / 100);
+ }
+
+ public static convertLevelToXp(level: number): number {
+ return 50 * level * level + 25 * level; // 50x² + 25x
+ }
+
+ public static genRandomizedXp(): number {
+ return Math.floor(Math.random() * (40 - 15 + 1)) + 15;
+ }
+}
diff --git a/lib/models/instance/ModLog.ts b/lib/models/instance/ModLog.ts
new file mode 100644
index 0000000..c25f043
--- /dev/null
+++ b/lib/models/instance/ModLog.ts
@@ -0,0 +1,127 @@
+import { type Snowflake } from 'discord.js';
+import { nanoid } from 'nanoid';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export enum ModLogType {
+ PERM_BAN = 'PERM_BAN',
+ TEMP_BAN = 'TEMP_BAN',
+ UNBAN = 'UNBAN',
+ KICK = 'KICK',
+ PERM_MUTE = 'PERM_MUTE',
+ TEMP_MUTE = 'TEMP_MUTE',
+ UNMUTE = 'UNMUTE',
+ WARN = 'WARN',
+ PERM_PUNISHMENT_ROLE = 'PERM_PUNISHMENT_ROLE',
+ TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE',
+ REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE',
+ PERM_CHANNEL_BLOCK = 'PERM_CHANNEL_BLOCK',
+ TEMP_CHANNEL_BLOCK = 'TEMP_CHANNEL_BLOCK',
+ CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK',
+ TIMEOUT = 'TIMEOUT',
+ REMOVE_TIMEOUT = 'REMOVE_TIMEOUT'
+}
+
+export interface ModLogModel {
+ id: string;
+ type: ModLogType;
+ user: Snowflake;
+ moderator: Snowflake;
+ reason: string | null;
+ duration: number | null;
+ guild: Snowflake;
+ evidence: string;
+ pseudo: boolean;
+ hidden: boolean;
+}
+
+export interface ModLogModelCreationAttributes {
+ id?: string;
+ type: ModLogType;
+ user: Snowflake;
+ moderator: Snowflake;
+ reason?: string | null;
+ duration?: number;
+ guild: Snowflake;
+ evidence?: string;
+ pseudo?: boolean;
+ hidden?: boolean;
+}
+
+/**
+ * A mod log case.
+ */
+export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> implements ModLogModel {
+ /**
+ * The primary key of the modlog entry.
+ */
+ public declare id: string;
+
+ /**
+ * The type of punishment.
+ */
+ public declare type: ModLogType;
+
+ /**
+ * The user being punished.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The user carrying out the punishment.
+ */
+ public declare moderator: Snowflake;
+
+ /**
+ * The reason the user is getting punished.
+ */
+ public declare reason: string | null;
+
+ /**
+ * The amount of time the user is getting punished for.
+ */
+ public declare duration: number | null;
+
+ /**
+ * The guild the user is getting punished in.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * Evidence of what the user is getting punished for.
+ */
+ public declare evidence: string;
+
+ /**
+ * Not an actual modlog just used so a punishment entry can be made.
+ */
+ public declare pseudo: boolean;
+
+ /**
+ * Hides from the modlog command unless show hidden is specified.
+ */
+ public declare hidden: boolean;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ ModLog.init(
+ {
+ id: { type: DataTypes.STRING, primaryKey: true, allowNull: false, defaultValue: nanoid },
+ type: { type: DataTypes.STRING, allowNull: false }, //? This is not an enum because of a sequelize issue: https://github.com/sequelize/sequelize/issues/2554
+ user: { type: DataTypes.STRING, allowNull: false },
+ moderator: { type: DataTypes.STRING, allowNull: false },
+ duration: { type: DataTypes.STRING, allowNull: true },
+ reason: { type: DataTypes.TEXT, allowNull: true },
+ guild: { type: DataTypes.STRING, references: { model: 'Guilds', key: 'id' } },
+ evidence: { type: DataTypes.TEXT, allowNull: true },
+ pseudo: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
+ hidden: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/instance/Reminder.ts b/lib/models/instance/Reminder.ts
new file mode 100644
index 0000000..964ea63
--- /dev/null
+++ b/lib/models/instance/Reminder.ts
@@ -0,0 +1,84 @@
+import { Snowflake } from 'discord.js';
+import { nanoid } from 'nanoid';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface ReminderModel {
+ id: string;
+ user: Snowflake;
+ messageUrl: string;
+ content: string;
+ created: Date;
+ expires: Date;
+ notified: boolean;
+}
+
+export interface ReminderModelCreationAttributes {
+ id?: string;
+ user: Snowflake;
+ messageUrl: string;
+ content: string;
+ created: Date;
+ expires: Date;
+ notified?: boolean;
+}
+
+/**
+ * Represents a reminder the a user has set.
+ */
+export class Reminder extends BaseModel<ReminderModel, ReminderModelCreationAttributes> implements ReminderModel {
+ /**
+ * The id of the reminder.
+ */
+ public declare id: string;
+
+ /**
+ * The user that the reminder is for.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The url of the message where the reminder was created.
+ */
+ public declare messageUrl: string;
+
+ /**
+ * The content of the reminder.
+ */
+ public declare content: string;
+
+ /**
+ * The date the reminder was created.
+ */
+ public declare created: Date;
+
+ /**
+ * The date when the reminder expires.
+ */
+ public declare expires: Date;
+
+ /**
+ * Whether the user has been notified about the reminder.
+ */
+ public declare notified: boolean;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Reminder.init(
+ {
+ id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid },
+ user: { type: DataTypes.STRING, allowNull: false },
+ messageUrl: { type: DataTypes.STRING, allowNull: false },
+ content: { type: DataTypes.TEXT, allowNull: false },
+ created: { type: DataTypes.DATE, allowNull: false },
+ expires: { type: DataTypes.DATE, allowNull: false },
+ notified: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/instance/StickyRole.ts b/lib/models/instance/StickyRole.ts
new file mode 100644
index 0000000..00e98ce
--- /dev/null
+++ b/lib/models/instance/StickyRole.ts
@@ -0,0 +1,58 @@
+import { type Snowflake } from 'discord.js';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface StickyRoleModel {
+ user: Snowflake;
+ guild: Snowflake;
+ roles: Snowflake[];
+ nickname: string;
+}
+export interface StickyRoleModelCreationAttributes {
+ user: Snowflake;
+ guild: Snowflake;
+ roles: Snowflake[];
+ nickname?: string;
+}
+
+/**
+ * Information about a user's roles and nickname when they leave a guild.
+ */
+export class StickyRole extends BaseModel<StickyRoleModel, StickyRoleModelCreationAttributes> implements StickyRoleModel {
+ /**
+ * The id of the user the roles belongs to.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The guild where this should happen.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * The roles that the user should have returned
+ */
+ public declare roles: Snowflake[];
+
+ /**
+ * The user's previous nickname
+ */
+ public declare nickname: string;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ StickyRole.init(
+ {
+ user: { type: DataTypes.STRING, allowNull: false },
+ guild: { type: DataTypes.STRING, allowNull: false },
+ roles: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ nickname: { type: DataTypes.STRING, allowNull: true }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/shared/Global.ts b/lib/models/shared/Global.ts
new file mode 100644
index 0000000..b1aa0cc
--- /dev/null
+++ b/lib/models/shared/Global.ts
@@ -0,0 +1,67 @@
+import { type Snowflake } from 'discord.js';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface GlobalModel {
+ environment: 'production' | 'development' | 'beta';
+ disabledCommands: string[];
+ blacklistedUsers: Snowflake[];
+ blacklistedGuilds: Snowflake[];
+ blacklistedChannels: Snowflake[];
+}
+
+export interface GlobalModelCreationAttributes {
+ environment: 'production' | 'development' | 'beta';
+ disabledCommands?: string[];
+ blacklistedUsers?: Snowflake[];
+ blacklistedGuilds?: Snowflake[];
+ blacklistedChannels?: Snowflake[];
+}
+
+/**
+ * Data specific to a certain instance of the bot.
+ */
+export class Global extends BaseModel<GlobalModel, GlobalModelCreationAttributes> implements GlobalModel {
+ /**
+ * The bot's environment.
+ */
+ public declare environment: 'production' | 'development' | 'beta';
+
+ /**
+ * Globally disabled commands.
+ */
+ public declare disabledCommands: string[];
+
+ /**
+ * Globally blacklisted users.
+ */
+ public declare blacklistedUsers: Snowflake[];
+
+ /**
+ * Guilds blacklisted from using the bot.
+ */
+ public declare blacklistedGuilds: Snowflake[];
+
+ /**
+ * Channels where the bot is prevented from running commands in.
+ */
+ public declare blacklistedChannels: Snowflake[];
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Global.init(
+ {
+ environment: { type: DataTypes.STRING, primaryKey: true },
+ disabledCommands: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedGuilds: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/lib/models/shared/GuildCount.ts b/lib/models/shared/GuildCount.ts
new file mode 100644
index 0000000..7afef56
--- /dev/null
+++ b/lib/models/shared/GuildCount.ts
@@ -0,0 +1,38 @@
+import { DataTypes, Model, type Sequelize } from 'sequelize';
+import { Environment } from '../../../config/Config.js';
+
+export interface GuildCountModel {
+ timestamp: Date;
+ environment: Environment;
+ guildCount: number;
+}
+
+export interface GuildCountCreationAttributes {
+ timestamp?: Date;
+ environment: Environment;
+ guildCount: number;
+}
+
+/**
+ * The number of guilds that the bot is in for each environment.
+ */
+export class GuildCount extends Model<GuildCountModel, GuildCountCreationAttributes> implements GuildCountModel {
+ public declare timestamp: Date;
+ public declare environment: Environment;
+ public declare guildCount: number;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ GuildCount.init(
+ {
+ timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
+ environment: { type: DataTypes.STRING, allowNull: false },
+ guildCount: { type: DataTypes.BIGINT, allowNull: false }
+ },
+ { sequelize, timestamps: false }
+ );
+ }
+}
diff --git a/lib/models/shared/MemberCount.ts b/lib/models/shared/MemberCount.ts
new file mode 100644
index 0000000..200a58e
--- /dev/null
+++ b/lib/models/shared/MemberCount.ts
@@ -0,0 +1,37 @@
+import { DataTypes, Model, type Sequelize } from 'sequelize';
+
+export interface MemberCountModel {
+ timestamp: Date;
+ guildId: string;
+ memberCount: number;
+}
+
+export interface MemberCountCreationAttributes {
+ timestamp?: Date;
+ guildId: string;
+ memberCount: number;
+}
+
+/**
+ * The member count of each guild that the bot is in that have over 100 members.
+ */
+export class MemberCount extends Model<MemberCountModel, MemberCountCreationAttributes> implements MemberCountModel {
+ public declare timestamp: Date;
+ public declare guildId: string;
+ public declare memberCount: number;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ MemberCount.init(
+ {
+ timestamp: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
+ guildId: { type: DataTypes.STRING, allowNull: false },
+ memberCount: { type: DataTypes.BIGINT, allowNull: false }
+ },
+ { sequelize, timestamps: false }
+ );
+ }
+}
diff --git a/lib/models/shared/Shared.ts b/lib/models/shared/Shared.ts
new file mode 100644
index 0000000..dec77d1
--- /dev/null
+++ b/lib/models/shared/Shared.ts
@@ -0,0 +1,84 @@
+import { Snowflake } from 'discord.js';
+import type { Sequelize } from 'sequelize';
+import { BadWords } from '../../automod/AutomodShared.js';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface SharedModel {
+ primaryKey: 0;
+ superUsers: Snowflake[];
+ privilegedUsers: Snowflake[];
+ badLinksSecret: string[];
+ badLinks: string[];
+ badWords: BadWords;
+ autoBanCode: string | null;
+}
+
+export interface SharedModelCreationAttributes {
+ primaryKey?: 0;
+ superUsers?: Snowflake[];
+ privilegedUsers?: Snowflake[];
+ badLinksSecret?: string[];
+ badLinks?: string[];
+ badWords?: BadWords;
+ autoBanCode?: string;
+}
+
+/**
+ * Data shared between all bot instances.
+ */
+export class Shared extends BaseModel<SharedModel, SharedModelCreationAttributes> implements SharedModel {
+ /**
+ * The primary key of the shared model.
+ */
+ public declare primaryKey: 0;
+
+ /**
+ * Trusted users.
+ */
+ public declare superUsers: Snowflake[];
+
+ /**
+ * Users that have all permissions that devs have except eval.
+ */
+ public declare privilegedUsers: Snowflake[];
+
+ /**
+ * Non-public bad links.
+ */
+ public declare badLinksSecret: string[];
+
+ /**
+ * Public Bad links.
+ */
+ public declare badLinks: string[];
+
+ /**
+ * Bad words.
+ */
+ public declare badWords: BadWords;
+
+ /**
+ * Code that is used to match for auto banning users in moulberry's bush
+ */
+ public declare autoBanCode: string;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Shared.init(
+ {
+ primaryKey: { type: DataTypes.INTEGER, primaryKey: true, validate: { min: 0, max: 0 } },
+ superUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ privilegedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ badLinksSecret: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ badLinks: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ badWords: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} },
+ autoBanCode: { type: DataTypes.TEXT }
+ },
+ { sequelize, freezeTableName: true }
+ );
+ }
+}
diff --git a/lib/models/shared/Stat.ts b/lib/models/shared/Stat.ts
new file mode 100644
index 0000000..8e2e0b3
--- /dev/null
+++ b/lib/models/shared/Stat.ts
@@ -0,0 +1,72 @@
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+type Environment = 'production' | 'development' | 'beta';
+
+export interface StatModel {
+ environment: Environment;
+ commandsUsed: bigint;
+ slashCommandsUsed: bigint;
+}
+
+export interface StatModelCreationAttributes {
+ environment: Environment;
+ commandsUsed?: bigint;
+ slashCommandsUsed?: bigint;
+}
+
+/**
+ * Statistics for each instance of the bot.
+ */
+export class Stat extends BaseModel<StatModel, StatModelCreationAttributes> implements StatModel {
+ /**
+ * The bot's environment.
+ */
+ public declare environment: Environment;
+
+ /**
+ * The number of commands used
+ */
+ public declare commandsUsed: bigint;
+
+ /**
+ * The number of slash commands used
+ */
+ public declare slashCommandsUsed: bigint;
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Stat.init(
+ {
+ environment: { type: DataTypes.STRING, primaryKey: true },
+ commandsUsed: {
+ type: DataTypes.TEXT,
+ get: function (): bigint {
+ return BigInt(this.getDataValue('commandsUsed'));
+ },
+ set: function (val: bigint) {
+ return this.setDataValue('commandsUsed', <any>`${val}`);
+ },
+ allowNull: false,
+ defaultValue: `${0n}`
+ },
+ slashCommandsUsed: {
+ type: DataTypes.TEXT,
+ get: function (): bigint {
+ return BigInt(this.getDataValue('slashCommandsUsed'));
+ },
+ set: function (val: bigint) {
+ return this.setDataValue('slashCommandsUsed', <any>`${val}`);
+ },
+ allowNull: false,
+ defaultValue: `${0n}`
+ }
+ },
+ { sequelize }
+ );
+ }
+}