Allow replies during room creation cooldowns
This commit is contained in:
1 parent
c630303b47
commit
910576921b
3 files changed
+55
-6
No files matched your search
@@ -582,7 +582,7 @@ export class ChatService {
|
||||
async sendChatMessage(chatUrlOrRoomId: string, message: string): Promise<boolean> {
|
||||
if (!this.accountAuth?.isFullyAuthenticated()) return false;
|
||||
const active = this.getInvitationCooldown();
|
||||
if (active) {
|
||||
if (active && this.blocksExistingRoomSends(active)) {
|
||||
const secondsLeft = Math.ceil((active.until - Date.now()) / 1000);
|
||||
this.logger.warn(
|
||||
`[chat] skipping sendChatMessage — send cooldown active for ${secondsLeft}s (${active.redditCode ?? 'rate-limited'})`,
|
||||
@@ -607,7 +607,7 @@ export class ChatService {
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof MatrixRateLimitError) {
|
||||
this.setInvitationCooldown(error);
|
||||
this.setInvitationCooldown(error, 'all-sends');
|
||||
return false;
|
||||
}
|
||||
this.logger.error('[chat] sendChatMessage failed:', error);
|
||||
@@ -690,7 +690,7 @@ export class ChatService {
|
||||
return { status: 'sent', chat };
|
||||
} catch (error) {
|
||||
if (error instanceof MatrixRateLimitError) {
|
||||
this.setInvitationCooldown(error);
|
||||
this.setInvitationCooldown(error, 'room-creation');
|
||||
const cooldown = this.getInvitationCooldown();
|
||||
return {
|
||||
status: 'retryable',
|
||||
@@ -758,17 +758,21 @@ export class ChatService {
|
||||
}
|
||||
|
||||
/** Record a cooldown after a `MatrixRateLimitError`. Test seam + reuse. */
|
||||
setInvitationCooldown(error: MatrixRateLimitError): void {
|
||||
setInvitationCooldown(
|
||||
error: MatrixRateLimitError,
|
||||
scope: NonNullable<InvitationCooldown['scope']> = this.inferCooldownScope(error.redditCode),
|
||||
): void {
|
||||
const now = Date.now();
|
||||
// Add a small safety margin so we don't probe again the very second
|
||||
// the homeserver thinks the window expired.
|
||||
const margin = 5_000;
|
||||
this.invitationCooldown = {
|
||||
until: now + error.retryAfterMs + margin,
|
||||
scope,
|
||||
redditCode: error.redditCode,
|
||||
reason:
|
||||
error.redditCode === 'rate.invitation_limit_mature_account'
|
||||
? "Reddit is throttling new chat invitations (\"you've created a lot of chats, take a break\")."
|
||||
scope === 'room-creation'
|
||||
? 'Reddit is throttling new chat invitations; replies in existing chats can continue.'
|
||||
: `Matrix rate-limited: ${error.errcode ?? '429'}`,
|
||||
setAt: now,
|
||||
};
|
||||
@@ -780,6 +784,17 @@ export class ChatService {
|
||||
void this.cache.persistCooldown();
|
||||
}
|
||||
|
||||
private blocksExistingRoomSends(cooldown: InvitationCooldown): boolean {
|
||||
return (cooldown.scope ?? this.inferCooldownScope(cooldown.redditCode)) === 'all-sends';
|
||||
}
|
||||
|
||||
private inferCooldownScope(redditCode: string | null): NonNullable<InvitationCooldown['scope']> {
|
||||
return redditCode === 'rate.room_creation_limit'
|
||||
|| redditCode === 'rate.invitation_limit_mature_account'
|
||||
? 'room-creation'
|
||||
: 'all-sends';
|
||||
}
|
||||
|
||||
// ------------ internals ------------
|
||||
|
||||
/** Single delta /sync that updates cache + nextBatch and returns the union. */
|
||||
|
||||
@@ -37,6 +37,11 @@ export interface ActiveChat {
|
||||
export interface InvitationCooldown {
|
||||
/** Wall-clock ms when sends may resume. */
|
||||
until: number;
|
||||
/**
|
||||
* Operation class affected by the homeserver limit. Optional for caches
|
||||
* written before cooldowns distinguished new rooms from existing-room sends.
|
||||
*/
|
||||
scope?: 'room-creation' | 'all-sends';
|
||||
/** Reddit-side error code, e.g. `rate.invitation_limit_mature_account`. */
|
||||
redditCode: string | null;
|
||||
/** Human-readable explanation suitable for the dashboard. */
|
||||
|
||||
@@ -115,6 +115,35 @@ describe('ChatService invitation cooldown', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('allows replies in existing rooms during a room-creation cooldown', async () => {
|
||||
const { svc, matrix } = buildService();
|
||||
matrix.sendTextMessage = async () => '$reply';
|
||||
svc.setInvitationCooldown(
|
||||
new MatrixRateLimitError(
|
||||
'429',
|
||||
60_000,
|
||||
'M_LIMIT_EXCEEDED',
|
||||
'rate.room_creation_limit',
|
||||
),
|
||||
);
|
||||
// Simulate a cooldown persisted before operation scopes were recorded.
|
||||
delete svc.getInvitationCooldown()!.scope;
|
||||
|
||||
await expect(svc.sendChatMessage('!room:example.test', 'hi')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('treats a generic limit raised while creating a room as room-creation-only', async () => {
|
||||
const { svc, matrix } = buildService();
|
||||
matrix.sendInitialDirectMessage = async () => {
|
||||
throw new MatrixRateLimitError('429', 60_000, 'M_LIMIT_EXCEEDED', null);
|
||||
};
|
||||
matrix.sendTextMessage = async () => '$reply';
|
||||
|
||||
await expect(svc.startConversationDetailed('someuser', 'opener', 't2_someuser'))
|
||||
.resolves.toMatchObject({ status: 'retryable' });
|
||||
await expect(svc.sendChatMessage('!room:example.test', 'reply')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('persists the cooldown to disk and restores it after a fresh service load', async () => {
|
||||
const logger = createMockLogger();
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-cooldown-'));
|
||||
|
||||
Reference in new issue
Block a user