Skip to content

Commit 32ad461

Browse files
committed
feat: add timeout support
1 parent 14e3470 commit 32ad461

File tree

13 files changed

+134
-12
lines changed

13 files changed

+134
-12
lines changed

scripts/SetMigrations.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ VALUES
101101
(1707505580449, 'V75SplitModerationType1707505580449'),
102102
(1707558132765, 'V76AddOptOutUnknownMessageLogging1707558132765'),
103103
(1707605222927, 'V77AddEventsIncludeBots1707605222927'),
104-
(1707642380524, 'V78AddVoiceActivityLogging1707642380524');
104+
(1707642380524, 'V78AddVoiceActivityLogging1707642380524'),
105+
(1708164874479, 'V79AddTimeout1708164874479');
105106

106107
COMMIT;

src/languages/en-US/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
"disabledChannels": "A list of channels for disabled commands, for example, setting up a channel called general will forbid all users from using my commands there. Moderators+ override this purposely to allow them to moderate without switching channels.",
3434
"disabledCommands": "The disabled commands, core commands may not be disabled, and moderators will override this. All commands must be in lower case.",
3535
"disableNaturalPrefix": "Whether or not I should listen for my natural prefix, `Skyra,`",
36-
"eventsBanAdd": "This event posts anonymous moderation logs when a user gets banned. You must set up `channels.moderation-logs`.",
37-
"eventsBanRemove": "This event posts anonymous moderation logs when a user gets unbanned. You must set up `channels.moderation-logs`.",
36+
"eventsBanAdd": "This event posts non-bot moderation logs when a user gets banned. You must set up `channels.moderation-logs`.",
37+
"eventsBanRemove": "This event posts non-bot moderation logs when a user gets unbanned. You must set up `channels.moderation-logs`.",
38+
"eventsTimeout": "This event posts non-bot moderation logs when a user's timeout status changes. You must set up `channels.moderation-logs`.",
3839
"eventsUnknownMessages": "Whether or not I should post updates on unknown command messages.",
3940
"eventsTwemojiReactions": "Whether or not twemoji reactions are posted in the reaction logs channel.",
4041
"eventsIncludeBots": "Whether or not I should ignore bots in the server logs.",

src/lib/database/entities/GuildEntity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ export class GuildEntity extends BaseEntity implements IBaseEntity {
180180
@Column('boolean', { name: 'events.ban-remove', default: false })
181181
public eventsBanRemove = false;
182182

183+
@ConfigurableKey({ description: LanguageKeys.Settings.EventsTimeout })
184+
@Column('boolean', { name: 'events.timeout', default: false })
185+
public eventsTimeout = false;
186+
183187
@ConfigurableKey({ description: LanguageKeys.Settings.EventsUnknownMessages })
184188
@Column('boolean', { name: 'events.unknown-messages', default: false })
185189
public eventsUnknownMessages = false;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const BanAdd = 'eventsBanAdd';
22
export const BanRemove = 'eventsBanRemove';
3+
export const Timeout = 'eventsTimeout';
34
export const UnknownMessages = 'eventsUnknownMessages';
45
export const IncludeTwemoji = 'eventsTwemojiReactions';
56
export const IncludeBots = 'eventsIncludeBots';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm';
2+
3+
export class V79AddTimeout1708164874479 implements MigrationInterface {
4+
public async up(queryRunner: QueryRunner): Promise<void> {
5+
await queryRunner.addColumn('guilds', new TableColumn({ name: 'events.timeout', type: 'boolean', default: false }));
6+
}
7+
8+
public async down(queryRunner: QueryRunner): Promise<void> {
9+
await queryRunner.dropColumn('guilds', 'events.timeout');
10+
}
11+
}

src/lib/i18n/languageKeys/keys/Settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const DisableNaturalPrefix = T('settings:disableNaturalPrefix');
77
export const DisabledChannels = T('settings:disabledChannels');
88
export const EventsBanAdd = T('settings:eventsBanAdd');
99
export const EventsBanRemove = T('settings:eventsBanRemove');
10+
export const EventsTimeout = T('settings:eventsTimeout');
1011
export const EventsUnknownMessages = T('settings:eventsUnknownMessages');
1112
export const EventsTwemojiReactions = T('settings:eventsTwemojiReactions');
1213
export const MessagesIgnoreChannels = T('settings:messagesIgnoreChannels');

src/lib/moderation/managers/LoggerManager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { writeSettings, type GuildSettingsOfType } from '#lib/database';
2-
import { LoggerTypeManager } from '#lib/moderation/managers/LoggerTypeManager';
2+
import { LoggerTypeManager, type LoggerTypeContext } from '#lib/moderation/managers/LoggerTypeManager';
33
import { toErrorCodeResult } from '#utils/common';
44
import { getCodeStyle, getLogPrefix } from '#utils/functions/pieces';
55
import { EmbedBuilder } from '@discordjs/builders';
@@ -8,6 +8,7 @@ import { isFunction, isNullish, isNullishOrEmpty, type Awaitable, type Nullish }
88
import { PermissionFlagsBits, RESTJSONErrorCodes, type Guild, type GuildBasedChannel, type MessageCreateOptions, type Snowflake } from 'discord.js';
99

1010
export class LoggerManager {
11+
public readonly timeout = new LoggerTypeManager<TimeoutLoggerTypeContext>(this);
1112
public readonly prune = new LoggerTypeManager(this);
1213

1314
#guild: Guild;
@@ -121,3 +122,8 @@ export interface LoggerManagerSendOptions {
121122
export type LoggerManagerSendMessageOptions = MessageCreateOptions | EmbedBuilder | EmbedBuilder[];
122123

123124
const LogPrefix = getLogPrefix('LoggerManager');
125+
126+
export interface TimeoutLoggerTypeContext extends LoggerTypeContext {
127+
oldValue: number | null;
128+
newValue: number | null;
129+
}

src/lib/moderation/managers/LoggerTypeManager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ export class LoggerTypeManager<Entry extends LoggerTypeContext = LoggerTypeConte
3636
this.#manager = manager;
3737
}
3838

39+
/**
40+
* Returns whether or not the context data is set, which will always be true
41+
* when an action was taken by Skyra.
42+
* @param id The ID of the context data to check.
43+
*/
44+
public isSet(id: Snowflake) {
45+
return this.#context.has(id);
46+
}
47+
3948
/**
4049
* Wait for the context data to be set.
4150
* @param id The ID of the context data to wait for.
@@ -104,4 +113,5 @@ export class LoggerTypeManager<Entry extends LoggerTypeContext = LoggerTypeConte
104113

105114
export interface LoggerTypeContext {
106115
userId: Snowflake;
116+
reason?: string;
107117
}

src/listeners/guilds/members/guildMemberUpdateNicknameNotify.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export class UserListener extends Listener {
3939
previousName === null
4040
? LanguageKeys.Events.Guilds.Members.NameUpdatePreviousWasNotSet
4141
: LanguageKeys.Events.Guilds.Members.NameUpdatePreviousWasSet,
42-
{
43-
previousName
44-
}
42+
{ previousName }
4543
),
4644
t(
4745
nextName === null
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { GuildSettings, readSettings } from '#lib/database';
2+
import { Events } from '#lib/types';
3+
import { getLogger, getModeration } from '#utils/functions';
4+
import { TypeMetadata, TypeVariation } from '#utils/moderationConstants';
5+
import { ApplyOptions } from '@sapphire/decorators';
6+
import { Listener } from '@sapphire/framework';
7+
import { isNumber } from '@sapphire/utilities';
8+
import type { GuildMember } from 'discord.js';
9+
10+
@ApplyOptions<Listener.Options>({ event: Events.GuildMemberUpdate })
11+
export class UserListener extends Listener {
12+
public async run(previous: GuildMember, next: GuildMember) {
13+
const prevTimeout = this.#getTimeout(previous);
14+
const nextTimeout = this.#getTimeout(next);
15+
if (prevTimeout === nextTimeout) return;
16+
17+
const { user, guild } = next;
18+
const logger = getLogger(guild);
19+
const actionBySkyra = logger.timeout.isSet(guild.id);
20+
const contextPromise = logger.timeout.wait(guild.id);
21+
22+
// If the action was done by Skyra, or external timeout is enabled, create a moderation action:
23+
if (actionBySkyra || (await readSettings(next, GuildSettings.Events.Timeout))) {
24+
const context = await contextPromise;
25+
const moderation = getModeration(guild);
26+
await moderation.waitLock();
27+
await moderation
28+
.create({
29+
userId: user.id,
30+
moderatorId: context?.userId ?? this.container.client.id!,
31+
type: TypeVariation.Timeout,
32+
metadata: nextTimeout ? TypeMetadata.Temporary : TypeMetadata.Appeal,
33+
duration: nextTimeout,
34+
reason: context?.reason
35+
})
36+
.create();
37+
}
38+
}
39+
40+
#getTimeout(member: GuildMember) {
41+
const timeout = member.communicationDisabledUntilTimestamp;
42+
return isNumber(timeout) && timeout >= Date.now() ? timeout : null;
43+
}
44+
}

0 commit comments

Comments
 (0)