import type {
BushClient,
BushGuildMember,
BushGuildMemberManager,
BushTextChannel,
BushUser,
BushUserResolvable,
GuildFeatures,
GuildLogType,
GuildModel
} from '#lib';
import { Guild, MessagePayload, type MessageOptions, type UserResolvable } from 'discord.js';
import type { RawGuildData } from 'discord.js/typings/rawDataTypes';
import _ from 'lodash';
import { Moderation } from '../../common/Moderation.js';
import { Guild as GuildDB } from '../../models/Guild.js';
import { ModLogType } from '../../models/ModLog.js';
/**
* Represents a guild (or a server) on Discord.
* It's recommended to see if a guild is available before performing operations or reading data from it. You can
* check this with {@link Guild.available}.
*/
export class BushGuild extends Guild {
public declare readonly client: BushClient;
public declare readonly me: BushGuildMember | null;
public declare members: BushGuildMemberManager;
public constructor(client: BushClient, data: RawGuildData) {
super(client, data);
}
/**
* Checks if the guild has a certain custom feature.
* @param feature The feature to check for
*/
public async hasFeature(feature: GuildFeatures): Promise {
const features = await this.getSetting('enabledFeatures');
return features.includes(feature);
}
/**
* Adds a custom feature to the guild.
* @param feature The feature to add
* @param moderator The moderator responsible for adding a feature
*/
public async addFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise {
const features = await this.getSetting('enabledFeatures');
const newFeatures = util.addOrRemoveFromArray('add', features, feature);
return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures;
}
/**
* Removes a custom feature from the guild.
* @param feature The feature to remove
* @param moderator The moderator responsible for removing a feature
*/
public async removeFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise {
const features = await this.getSetting('enabledFeatures');
const newFeatures = util.addOrRemoveFromArray('remove', features, feature);
return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures;
}
/**
* Makes a custom feature the opposite of what it was before
* @param feature The feature to toggle
* @param moderator The moderator responsible for toggling a feature
*/
public async toggleFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise {
return (await this.hasFeature(feature))
? await this.removeFeature(feature, moderator)
: await this.addFeature(feature, moderator);
}
/**
* Fetches a custom setting for the guild
* @param setting The setting to get
*/
public async getSetting(setting: K): Promise {
return (
client.cache.guilds.get(this.id)?.[setting] ??
((await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }))[setting]
);
}
/**
* Sets a custom setting for the guild
* @param setting The setting to change
* @param value The value to change the setting to
* @param moderator The moderator to responsible for changing the setting
*/
public async setSetting>(
setting: K,
value: GuildDB[K],
moderator?: BushGuildMember
): Promise {
const row = (await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id });
const oldValue = row[setting] as GuildDB[K];
row[setting] = value;
client.cache.guilds.set(this.id, row.toJSON() as GuildDB);
client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator);
return await row.save();
}
/**
* Get a the log channel configured for a certain log type.
* @param logType The type of log channel to get.
* @returns Either the log channel or undefined if not configured.
*/
public async getLogChannel(logType: GuildLogType): Promise {
const channelId = (await this.getSetting('logChannels'))[logType];
if (!channelId) return undefined;
return (
(this.channels.cache.get(channelId) as BushTextChannel | undefined) ??
((await this.channels.fetch(channelId)) as BushTextChannel | null) ??
undefined
);
}
/**
* Bans a user, dms them, creates a mod log entry, and creates a punishment entry.
* @param options Options for banning the user.
* @returns A string status message of the ban.
*/
public async bushBan(options: BushBanOptions): Promise {
// checks
if (!this.me!.permissions.has('BAN_MEMBERS')) return 'missing permissions';
let caseID: string | undefined = undefined;
const user = (await util.resolveNonCachedUser(options.user))!;
const moderator = (await util.resolveNonCachedUser(options.moderator!)) ?? client.user!;
const ret = await (async () => {
await this.members.cache.get(user.id)?.punishDM('banned', options.reason, options.duration ?? 0);
// ban
const banSuccess = await this.bans
.create(user?.id ?? options.user, {
reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`,
days: options.deleteDays
})
.catch(() => false);
if (!banSuccess) return 'error banning';
// add modlog entry
const { log: modlog } = await Moderation.createModLogEntry({
type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
user: user,
moderator: moderator.id,
reason: options.reason,
duration: options.duration,
guild: this,
evidence: options.evidence
});
if (!modlog) return 'error creating modlog entry';
caseID = modlog.id;
// add punishment entry so they can be unbanned later
const punishmentEntrySuccess = await Moderation.createPunishmentEntry({
type: 'ban',
user: user,
guild: this,
duration: options.duration,
modlog: modlog.id
});
if (!punishmentEntrySuccess) return 'error creating ban entry';
return 'success';
})();
if (!['error banning', 'error creating modlog entry', 'error creating ban entry'].includes(ret))
client.emit('bushBan', user, moderator, this, options.reason ?? undefined, caseID!, options.duration ?? 0);
return ret;
}
/**
* Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry.
* @param options Options for unbanning the user.
* @returns A status message of the unban.
*/
public async bushUnban(options: BushUnbanOptions): Promise {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
const user = (await util.resolveNonCachedUser(options.user))!;
const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.me))!;
const ret = await (async () => {
const bans = await this.bans.fetch();
let notBanned = false;
if (!bans.has(user.id)) notBanned = true;
const unbanSuccess = await this.bans
.remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`)
.catch((e) => {
if (e?.code === 'UNKNOWN_BAN') {
notBanned = true;
return true;
} else return false;
});
if (notBanned) return 'user not banned';
if (!unbanSuccess) return 'error unbanning';
// add modlog entry
const { log: modlog } = await Moderation.createModLogEntry({
type: ModLogType.UNBAN,
user: user.id,
moderator: moderator.id,
reason: options.reason,
guild: this
});
if (!modlog) return 'error creating modlog entry';
caseID = modlog.id;
// remove punishment entry
const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({
type: 'ban',
user: user.id,
guild: this
});
if (!removePunishmentEntrySuccess) return 'error removing ban entry';
const userObject = client.users.cache.get(user.id);
const dmSuccess = await userObject
?.send(
`You have been unbanned from **${util.discord.escapeMarkdown(this.toString())}** for **${
options.reason ?? 'No reason provided'
}**.`
)
.catch(() => false);
dmSuccessEvent = !!dmSuccess;
return 'success';
})();
if (!['error unbanning', 'error creating modlog entry', 'error removing ban entry'].includes(ret))
client.emit('bushUnban', user, moderator, this, options.reason ?? undefined, caseID!, dmSuccessEvent!);
return ret;
}
/**
* Sends a message to the guild's specified logging channel
* @param logType The corresponding channel that the message will be sent to
* @param message The parameters for {@link BushTextChannel.send}
*/
public async sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions) {
const logChannel = await this.getLogChannel(logType);
if (!logChannel || logChannel.type !== 'GUILD_TEXT') return;
if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return;
return await logChannel.send(message).catch(() => null);
}
/**
* Sends a formatted error message in a guild's error log channel
* @param title The title of the error embed
* @param message The description of the error embed
*/
public async error(title: string, message: string) {
void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] });
}
}
/**
* Options for unbanning a user
*/
interface BushUnbanOptions {
/**
* The user to unban
*/
user: BushUserResolvable | BushUser;
/**
* The reason for unbanning the user
*/
reason?: string | null;
/**
* The moderator who unbanned the user
*/
moderator?: BushUserResolvable;
}
/**
* Options for banning a user
*/
interface BushBanOptions {
/**
* The user to ban
*/
user: BushUserResolvable | UserResolvable;
/**
* The reason to ban the user
*/
reason?: string | null;
/**
* The moderator who banned the user
*/
moderator?: BushUserResolvable;
/**
* The duration of the ban
*/
duration?: number;
/**
* The number of days to delete the user's messages for
*/
deleteDays?: number;
/**
* The evidence for the ban
*/
evidence?: string;
}
type PunishmentResponse = 'success' | 'missing permissions' | 'error creating modlog entry';
/**
* Response returned when banning a user
*/
type BanResponse = PunishmentResponse | 'error banning' | 'error creating ban entry';
/**
* Response returned when unbanning a user
*/
type UnbanResponse = PunishmentResponse | 'user not banned' | 'error unbanning' | 'error removing ban entry';