feat: timeout support
This commit is contained in:
parent
06877e90cc
commit
39e0dfa27f
23 changed files with 532 additions and 92 deletions
|
@ -6,6 +6,7 @@ import { VCAlert } from "./entities/VCAlert";
|
|||
|
||||
interface GuildEventArgs extends Record<string, unknown[]> {
|
||||
expiredMute: [Mute];
|
||||
timeoutMuteToRenew: [Mute];
|
||||
scheduledPost: [ScheduledPost];
|
||||
reminder: [Reminder];
|
||||
expiredTempban: [Tempban];
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import moment from "moment-timezone";
|
||||
import { Brackets, getRepository, Repository } from "typeorm";
|
||||
import { DBDateFormat } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { Mute } from "./entities/Mute";
|
||||
import { MuteTypes } from "./MuteTypes";
|
||||
|
||||
export type AddMuteParams = {
|
||||
userId: Mute["user_id"];
|
||||
type: MuteTypes;
|
||||
expiresAt: number | null;
|
||||
rolesToRestore?: Mute["roles_to_restore"];
|
||||
muteRole?: string | null;
|
||||
timeoutExpiresAt?: number;
|
||||
};
|
||||
|
||||
export class GuildMutes extends BaseGuildRepository {
|
||||
private mutes: Repository<Mute>;
|
||||
|
@ -34,14 +45,18 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
return mute != null;
|
||||
}
|
||||
|
||||
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
||||
const expiresAt = expiryTime ? moment.utc().add(expiryTime, "ms").format("YYYY-MM-DD HH:mm:ss") : null;
|
||||
async addMute(params: AddMuteParams): Promise<Mute> {
|
||||
const expiresAt = params.expiresAt ? moment.utc(params.expiresAt).format(DBDateFormat) : null;
|
||||
const timeoutExpiresAt = params.timeoutExpiresAt ? moment.utc(params.timeoutExpiresAt).format(DBDateFormat) : null;
|
||||
|
||||
const result = await this.mutes.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
user_id: params.userId,
|
||||
type: params.type,
|
||||
expires_at: expiresAt,
|
||||
roles_to_restore: rolesToRestore ?? [],
|
||||
roles_to_restore: params.rolesToRestore ?? [],
|
||||
mute_role: params.muteRole,
|
||||
timeout_expires_at: timeoutExpiresAt,
|
||||
});
|
||||
|
||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||
|
@ -74,6 +89,32 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
}
|
||||
}
|
||||
|
||||
async updateExpiresAt(userId: string, timestamp: number | null): Promise<void> {
|
||||
const expiresAt = timestamp ? moment.utc(timestamp).format("YYYY-MM-DD HH:mm:ss") : null;
|
||||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async updateTimeoutExpiresAt(userId: string, timestamp: number): Promise<void> {
|
||||
const timeoutExpiresAt = moment.utc(timestamp).format(DBDateFormat);
|
||||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
timeout_expires_at: timeoutExpiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getActiveMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
|
@ -104,4 +145,16 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
user_id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
async fillMissingMuteRole(muteRole: string): Promise<void> {
|
||||
await this.mutes
|
||||
.createQueryBuilder()
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("type = :type", { type: MuteTypes.Role })
|
||||
.andWhere("mute_role IS NULL")
|
||||
.update({
|
||||
mute_role: muteRole,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
4
backend/src/data/MuteTypes.ts
Normal file
4
backend/src/data/MuteTypes.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export enum MuteTypes {
|
||||
Role = 1,
|
||||
Timeout = 2,
|
||||
}
|
|
@ -3,9 +3,14 @@ import { getRepository, Repository } from "typeorm";
|
|||
import { DAYS, DBDateFormat } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { Mute } from "./entities/Mute";
|
||||
import { MuteTypes } from "./MuteTypes";
|
||||
|
||||
const OLD_EXPIRED_MUTE_THRESHOLD = 7 * DAYS;
|
||||
|
||||
export const MAX_TIMEOUT_DURATION = 28 * DAYS;
|
||||
// When a timeout is under this duration but the mute expires later, the timeout will be reset to max duration
|
||||
export const TIMEOUT_RENEWAL_THRESHOLD = 21 * DAYS;
|
||||
|
||||
export class Mutes extends BaseRepository {
|
||||
private mutes: Repository<Mute>;
|
||||
|
||||
|
@ -14,7 +19,16 @@ export class Mutes extends BaseRepository {
|
|||
this.mutes = getRepository(Mute);
|
||||
}
|
||||
|
||||
async getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||
findMute(guildId: string, userId: string): Promise<Mute | undefined> {
|
||||
return this.mutes.findOne({
|
||||
where: {
|
||||
guild_id: guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSoonExpiringMutes(threshold: number): Promise<Mute[]> {
|
||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
|
@ -23,6 +37,16 @@ export class Mutes extends BaseRepository {
|
|||
.getMany();
|
||||
}
|
||||
|
||||
getTimeoutMutesToRenew(threshold: number): Promise<Mute[]> {
|
||||
const thresholdDateStr = moment.utc().add(threshold, "ms").format(DBDateFormat);
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
.andWhere("type = :type", { type: MuteTypes.Timeout })
|
||||
.andWhere("(expires_at IS NULL OR timeout_expires_at < expires_at)")
|
||||
.andWhere("timeout_expires_at <= :date", { date: thresholdDateStr })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async clearOldExpiredMutes(): Promise<void> {
|
||||
const thresholdDateStr = moment.utc().subtract(OLD_EXPIRED_MUTE_THRESHOLD, "ms").format(DBDateFormat);
|
||||
await this.mutes
|
||||
|
|
|
@ -10,6 +10,8 @@ export class Mute {
|
|||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column() type: number;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||
|
@ -17,4 +19,8 @@ export class Mute {
|
|||
@Column() case_id: number;
|
||||
|
||||
@Column("simple-array") roles_to_restore: string[];
|
||||
|
||||
@Column({ type: String, nullable: true }) mute_role: string | null;
|
||||
|
||||
@Column({ type: String, nullable: true }) timeout_expires_at: string | null;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// tslint:disable:no-console
|
||||
|
||||
import moment from "moment-timezone";
|
||||
import { lazyMemoize, MINUTES } from "../../utils";
|
||||
import { lazyMemoize, MINUTES, SECONDS } from "../../utils";
|
||||
import { Mute } from "../entities/Mute";
|
||||
import { emitGuildEvent, hasGuildEventListener } from "../GuildEvents";
|
||||
import { Mutes } from "../Mutes";
|
||||
import { Mutes, TIMEOUT_RENEWAL_THRESHOLD } from "../Mutes";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
const LOOP_INTERVAL = 15 * MINUTES;
|
||||
|
@ -16,14 +16,24 @@ function muteToKey(mute: Mute) {
|
|||
return `${mute.guild_id}/${mute.user_id}`;
|
||||
}
|
||||
|
||||
function broadcastExpiredMute(mute: Mute, tries = 0) {
|
||||
async function broadcastExpiredMute(guildId: string, userId: string, tries = 0) {
|
||||
const mute = await getMutesRepository().findMute(guildId, userId);
|
||||
if (!mute) {
|
||||
// Mute was already cleared
|
||||
return;
|
||||
}
|
||||
if (!mute.expires_at || moment(mute.expires_at).diff(moment()) > 10 * SECONDS) {
|
||||
// Mute duration was changed and it's no longer expiring now
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[EXPIRING MUTES LOOP] Broadcasting expired mute: ${mute.guild_id}/${mute.user_id}`);
|
||||
if (!hasGuildEventListener(mute.guild_id, "expiredMute")) {
|
||||
// If there are no listeners registered for the server yet, try again in a bit
|
||||
if (tries < MAX_TRIES_PER_SERVER) {
|
||||
timeouts.set(
|
||||
muteToKey(mute),
|
||||
setTimeout(() => broadcastExpiredMute(mute, tries + 1), 1 * MINUTES),
|
||||
setTimeout(() => broadcastExpiredMute(guildId, userId, tries + 1), 1 * MINUTES),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
@ -31,6 +41,21 @@ function broadcastExpiredMute(mute: Mute, tries = 0) {
|
|||
emitGuildEvent(mute.guild_id, "expiredMute", [mute]);
|
||||
}
|
||||
|
||||
function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
|
||||
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
|
||||
if (!hasGuildEventListener(mute.guild_id, "timeoutMuteToRenew")) {
|
||||
// If there are no listeners registered for the server yet, try again in a bit
|
||||
if (tries < MAX_TRIES_PER_SERVER) {
|
||||
timeouts.set(
|
||||
muteToKey(mute),
|
||||
setTimeout(() => broadcastTimeoutMuteToRenew(mute, tries + 1), 1 * MINUTES),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emitGuildEvent(mute.guild_id, "timeoutMuteToRenew", [mute]);
|
||||
}
|
||||
|
||||
export async function runExpiringMutesLoop() {
|
||||
console.log("[EXPIRING MUTES LOOP] Clearing old timeouts");
|
||||
for (const timeout of timeouts.values()) {
|
||||
|
@ -46,10 +71,16 @@ export async function runExpiringMutesLoop() {
|
|||
const remaining = Math.max(0, moment.utc(mute.expires_at!).diff(moment.utc()));
|
||||
timeouts.set(
|
||||
muteToKey(mute),
|
||||
setTimeout(() => broadcastExpiredMute(mute), remaining),
|
||||
setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[EXPIRING MUTES LOOP] Broadcasting timeout mutes to renew");
|
||||
const timeoutMutesToRenew = await getMutesRepository().getTimeoutMutesToRenew(TIMEOUT_RENEWAL_THRESHOLD);
|
||||
for (const mute of timeoutMutesToRenew) {
|
||||
broadcastTimeoutMuteToRenew(mute);
|
||||
}
|
||||
|
||||
console.log("[EXPIRING MUTES LOOP] Scheduling next loop");
|
||||
setTimeout(() => runExpiringMutesLoop(), LOOP_INTERVAL);
|
||||
}
|
||||
|
@ -69,7 +100,7 @@ export function registerExpiringMute(mute: Mute) {
|
|||
|
||||
timeouts.set(
|
||||
muteToKey(mute),
|
||||
setTimeout(() => broadcastExpiredMute(mute), remaining),
|
||||
setTimeout(() => broadcastExpiredMute(mute.guild_id, mute.user_id), remaining),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue