From c976519a1f86fa2edfe1c6b34e89c6683d488372 Mon Sep 17 00:00:00 2001 From: ispik Date: Thu, 21 May 2026 12:35:33 +0300 Subject: [PATCH 1/5] feat: Add the userslowmode event Signed-off-by: ispik --- src/Client.ts | 29 +++++++++++++++++++++++++++-- src/events/v1.ts | 23 ++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 3119525c..933e50c4 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -27,7 +27,7 @@ import { EventClient, type EventClientOptions, } from "./events/EventClient.js"; -import { ProtocolV1, handleEvent } from "./events/v1.js"; +import { ProtocolV1, handleEvent, UserSlowmodes } from "./events/v1.js"; import type { HydratedChannel } from "./hydration/channel.js"; import type { HydratedEmoji } from "./hydration/emoji.js"; import type { HydratedMessage } from "./hydration/message.js"; @@ -40,6 +40,7 @@ import { RE_MENTIONS, RE_SPOILER, } from "./lib/regex.js"; +import { ReactiveMap } from "@solid-primitives/map"; export type Session = { _id: string; token: string; user_id: string } | string; @@ -99,6 +100,8 @@ export type Events = { emojiCreate: [emoji: Emoji]; emojiDelete: [emoji: HydratedEmoji]; + + userSlowmodes: [] }; /** @@ -181,6 +184,7 @@ export class Client extends AsyncEventEmitter { readonly serverMembers; readonly sessions; readonly users; + readonly userSlowmodes; readonly api: API; readonly options: ClientOptions; @@ -199,7 +203,7 @@ export class Client extends AsyncEventEmitter { readonly connectionFailureCount: Accessor; #setConnectionFailureCount: Setter; #reconnectTimeout: number | undefined; - + #slowmodeTimers = new Map>(); /** * Create Stoat.js Client */ @@ -274,6 +278,7 @@ export class Client extends AsyncEventEmitter { this.serverMembers = new ServerMemberCollection(this); this.sessions = new SessionCollection(this); this.users = new UserCollection(this); + this.userSlowmodes = new ReactiveMap(); this.events = new EventClient(1, "json", this.options); this.events.on("error", (error) => this.emit("error", error)); @@ -282,6 +287,11 @@ export class Client extends AsyncEventEmitter { case ConnectionState.Connected: batch(() => { this.servers.forEach((server) => server.resetSyncStatus()); + for (const timer of this.#slowmodeTimers.values()) { + clearTimeout(timer); + } + this.#slowmodeTimers.clear(); + this.userSlowmodes.clear(); this.#setConnectionFailureCount(0); this.emit("connected"); }); @@ -578,4 +588,19 @@ export class Client extends AsyncEventEmitter { return data.id; } + + setSlowmode(channelId: string, data: UserSlowmodes): void { + const existing = this.#slowmodeTimers.get(channelId); + if (existing) clearTimeout(existing); + + this.userSlowmodes.set(channelId, { ...data, receivedAt: Date.now() }); + + const timer = setTimeout(() => { + this.userSlowmodes.delete(channelId); + this.#slowmodeTimers.delete(channelId); + this.emit("userSlowmodes"); + }, data.retry_after * 1000); + + this.#slowmodeTimers.set(channelId, timer); + } } diff --git a/src/events/v1.ts b/src/events/v1.ts index eb51b2f4..025acd4e 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -203,7 +203,11 @@ type ServerMessage = type: "UserMoveVoiceChannel"; node: string; token: string; - }; + } + | { + type: "UserSlowmodes"; + slowmodes: { channel_id: string; duration: number; retry_after: number }[]; + } /** * Policy change type @@ -235,6 +239,16 @@ type ChannelVoiceState = { participants: UserVoiceState[]; }; +/** + * Channel slowmodes for the active user + */ +export type UserSlowmodes = { + channel_id: string; + duration: number; + retry_after: number; + receivedAt?: number; +} + /** * Initial synchronisation packet */ @@ -984,5 +998,12 @@ export async function handleEvent( // todo break; } + case "UserSlowmodes": { + for (const slowmode of event.slowmodes) { + client.setSlowmode(slowmode.channel_id, slowmode); + } + client.emit("userSlowmodes"); + break; + } } } From ed873d874d60faf7e44d7ee50c87c531eb38aa4e Mon Sep 17 00:00:00 2001 From: ispik Date: Thu, 21 May 2026 12:43:11 +0300 Subject: [PATCH 2/5] chore: format Signed-off-by: ispik --- src/Client.ts | 6 +++--- src/events/EventClient.ts | 2 +- src/events/v1.ts | 10 +++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 933e50c4..bef6b3ae 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,6 +1,7 @@ import type { Accessor, Setter } from "solid-js"; import { batch, createSignal } from "solid-js"; +import { ReactiveMap } from "@solid-primitives/map"; import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter"; import { API } from "stoat-api"; import type { DataLogin, RevoltConfig, Role } from "stoat-api"; @@ -27,7 +28,7 @@ import { EventClient, type EventClientOptions, } from "./events/EventClient.js"; -import { ProtocolV1, handleEvent, UserSlowmodes } from "./events/v1.js"; +import { ProtocolV1, UserSlowmodes, handleEvent } from "./events/v1.js"; import type { HydratedChannel } from "./hydration/channel.js"; import type { HydratedEmoji } from "./hydration/emoji.js"; import type { HydratedMessage } from "./hydration/message.js"; @@ -40,7 +41,6 @@ import { RE_MENTIONS, RE_SPOILER, } from "./lib/regex.js"; -import { ReactiveMap } from "@solid-primitives/map"; export type Session = { _id: string; token: string; user_id: string } | string; @@ -101,7 +101,7 @@ export type Events = { emojiCreate: [emoji: Emoji]; emojiDelete: [emoji: HydratedEmoji]; - userSlowmodes: [] + userSlowmodes: []; }; /** diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index b77eff57..dd1e3866 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -95,7 +95,7 @@ export class EventClient< #connectTimeoutReference: number | undefined; #lastError: // eslint-disable-next-line @typescript-eslint/no-explicit-any - { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; + { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; /** * Create a new event client. diff --git a/src/events/v1.ts b/src/events/v1.ts index 025acd4e..c0ce2d89 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -206,8 +206,12 @@ type ServerMessage = } | { type: "UserSlowmodes"; - slowmodes: { channel_id: string; duration: number; retry_after: number }[]; - } + slowmodes: { + channel_id: string; + duration: number; + retry_after: number; + }[]; + }; /** * Policy change type @@ -247,7 +251,7 @@ export type UserSlowmodes = { duration: number; retry_after: number; receivedAt?: number; -} +}; /** * Initial synchronisation packet From e2bbf321c425f73eebb4535affeb566553e8e8bc Mon Sep 17 00:00:00 2001 From: ispik Date: Thu, 21 May 2026 20:35:20 +0300 Subject: [PATCH 3/5] chore: format Signed-off-by: ispik --- src/events/EventClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index dd1e3866..b77eff57 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -95,7 +95,7 @@ export class EventClient< #connectTimeoutReference: number | undefined; #lastError: // eslint-disable-next-line @typescript-eslint/no-explicit-any - { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; + { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; /** * Create a new event client. From 5e568a5d0856e54c9d4998d36a087f868cd8f790 Mon Sep 17 00:00:00 2001 From: ispik Date: Thu, 21 May 2026 21:48:16 +0300 Subject: [PATCH 4/5] refactor: satisfy review Signed-off-by: ispik --- src/Client.ts | 3 --- src/events/v1.ts | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index bef6b3ae..d9b117ea 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -287,9 +287,6 @@ export class Client extends AsyncEventEmitter { case ConnectionState.Connected: batch(() => { this.servers.forEach((server) => server.resetSyncStatus()); - for (const timer of this.#slowmodeTimers.values()) { - clearTimeout(timer); - } this.#slowmodeTimers.clear(); this.userSlowmodes.clear(); this.#setConnectionFailureCount(0); diff --git a/src/events/v1.ts b/src/events/v1.ts index c0ce2d89..a085e6b1 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -206,11 +206,7 @@ type ServerMessage = } | { type: "UserSlowmodes"; - slowmodes: { - channel_id: string; - duration: number; - retry_after: number; - }[]; + slowmodes: UserSlowmodes[]; }; /** From 9727411b2d8a20a82c297d8b2eba3feceaa38539 Mon Sep 17 00:00:00 2001 From: ispik Date: Thu, 21 May 2026 22:20:04 +0300 Subject: [PATCH 5/5] refactor: satisfy review Signed-off-by: ispik --- src/Client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index d9b117ea..480f085a 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -287,8 +287,6 @@ export class Client extends AsyncEventEmitter { case ConnectionState.Connected: batch(() => { this.servers.forEach((server) => server.resetSyncStatus()); - this.#slowmodeTimers.clear(); - this.userSlowmodes.clear(); this.#setConnectionFailureCount(0); this.emit("connected"); });