1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
|
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.
* <info>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}.</info>
*/
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<boolean> {
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<GuildModel['enabledFeatures']> {
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<GuildModel['enabledFeatures']> {
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<GuildModel['enabledFeatures']> {
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<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> {
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<K extends Exclude<keyof GuildModel, 'id'>>(
setting: K,
value: GuildDB[K],
moderator?: BushGuildMember
): Promise<GuildDB> {
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<BushTextChannel | undefined> {
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<BanResponse> {
// 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<UnbanResponse> {
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';
|