From 2c96f7f2583c7da7ad328d3141872a23a6a864e9 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 15:32:36 +0900 Subject: [PATCH 1/4] enhance(backend): increase MAX_ROOM_MEMBERS to 50 --- CHANGELOG.md | 1 + packages/backend/src/core/ChatService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98a09c66e..b22b0074db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 ### Server +- Enhance: チャットルームの最大メンバー数を30人から50人に調整 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 - Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 2ceff341cc..4e81847a52 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -29,7 +29,7 @@ import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -const MAX_ROOM_MEMBERS = 30; +const MAX_ROOM_MEMBERS = 50; const MAX_REACTIONS_PER_MESSAGE = 100; const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; From cbc53de8237fdfeee7c81effa819030962fa4b51 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 11 May 2025 15:37:46 +0900 Subject: [PATCH 2/4] fix: RateLimiterService (#13997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix rate limit check never ends * fix: long term / short term limitがないときでもそれぞれ用のnew Limiterとlimiter.getが呼ばれる問題 * refactor: wrap ratelimiter with promise * refactor: reimplement max/min with async * refactor: reimplement limit with async * refactor: do not check long term limit inside min * refactor: check if there is rate limit inside min/max function * refactor: remove unnecessary return in min/max function * refactor: remove unnecessary max/min function * refactor: return rate limit instead of throwing an object * fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 * fix lint error --------- Co-authored-by: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- CHANGELOG.md | 1 + .../backend/src/server/api/ApiCallService.ts | 22 ++-- .../src/server/api/RateLimiterService.ts | 118 ++++++++---------- .../src/server/api/SigninApiService.ts | 5 +- 4 files changed, 65 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b22b0074db..502cd80e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -666,6 +666,7 @@ - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 +- Fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 (#13997) - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 空文字列のリアクションはフォールバックされるように - Fix: リノートにリアクションできないように diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index a42fdaf730..7a4af407a3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - if ('info' in err) { - // errはLimiter.LimiterInfoであることが期待される - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }, err.info); - } else { - throw new TypeError('information must be a rate-limiter information.'); - } - }); + const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor); + if (rateLimit != null) { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }, rateLimit.info); + } } } diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 52d73baa0a..58fd6c7d8b 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type { IEndpointMeta } from './endpoints.js'; +type RateLimitInfo = { + code: 'BRIEF_REQUEST_INTERVAL', + info: Limiter.LimiterInfo, +} | { + code: 'RATE_LIMIT_EXCEEDED', + info: Limiter.LimiterInfo, +} + @Injectable() export class RateLimiterService { private logger: Logger; @@ -31,77 +39,57 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { - { - if (this.disabled) { - return Promise.resolve(); - } + private checkLimiter(options: Limiter.LimiterOption): Promise { + return new Promise((resolve, reject) => { + new Limiter(options).get((err, info) => { + if (err) { + return reject(err); + } + resolve(info); + }); + }); + } - // Short-term limit - const min = new Promise((ok, reject) => { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval! * factor, - max: 1, - db: this.redisClient, - }); + @bindThis + public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1): Promise { + if (this.disabled) { + return null; + } - minIntervalLimiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); - } else { - if (hasLongTermLimit) { - return max.then(ok, reject); - } else { - return ok(); - } - } - }); + // Short-term limit + if (limitation.minInterval != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval * factor, + max: 1, + db: this.redisClient, }); - // Long term limit - const max = new Promise((ok, reject) => { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration! * factor, - max: limitation.max! / factor, - db: this.redisClient, - }); + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - limiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); - } else { - return ok(); - } - }); - }); - - const hasShortTermLimit = typeof limitation.minInterval === 'number'; - - const hasLongTermLimit = - typeof limitation.duration === 'number' && - typeof limitation.max === 'number'; - - if (hasShortTermLimit) { - return min; - } else if (hasLongTermLimit) { - return max; - } else { - return Promise.resolve(); + if (info.remaining === 0) { + // eslint-disable-next-line no-throw-literal + return { code: 'BRIEF_REQUEST_INTERVAL', info }; } } + + // Long term limit + if (limitation.duration != null && limitation.max != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration, + max: limitation.max / factor, + db: this.redisClient, + }); + + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + // eslint-disable-next-line no-throw-literal + return { code: 'RATE_LIMIT_EXCEEDED', info }; + } + } + + return null; } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1d983ca4bc..3e889372d8 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -89,10 +89,9 @@ export class SigninApiService { return { error }; } - try { // not more than 1 attempt per second and not more than 10 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - } catch (err) { + const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + if (rateLimit != null) { reply.code(429); return { error: { From 5465dccda94fc933101965765d720e86b38f2840 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 15:39:39 +0900 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 502cd80e9a..9210a669ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Enhance: チャットルームの最大メンバー数を30人から50人に調整 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 - Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 +- Enhance: レートリミットの計算方法を調整 (#13997) - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 - Fix: ユーザ除外アンテナをインポートできない問題を修正 - Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正 @@ -666,7 +667,6 @@ - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 -- Fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 (#13997) - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 空文字列のリアクションはフォールバックされるように - Fix: リノートにリアクションできないように From 2ead6b8adc786877cd31bd9e1dd45ca60e488210 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 11 May 2025 15:40:58 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/global/MkSystemIcon.vue | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue index 66592da9c8..3454cdc9f2 100644 --- a/packages/frontend/src/components/global/MkSystemIcon.vue +++ b/packages/frontend/src/components/global/MkSystemIcon.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -87,14 +87,6 @@ const props = defineProps<{ transform: rotate(-90deg); } -.animCircleSuccess { - stroke-dasharray: var(--l); - stroke-dashoffset: var(--l); - animation: circleSuccess var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; - animation-delay: var(--delay, 0s); - transform-origin: center; -} - .animFade { opacity: 0; animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; @@ -112,19 +104,6 @@ const props = defineProps<{ } } -@keyframes circleSuccess { - 0% { - stroke-dashoffset: var(--l); - opacity: 0; - transform: rotate(-90deg); - } - 100% { - stroke-dashoffset: 0; - opacity: 1; - transform: rotate(90deg); - } -} - @keyframes fade-in { 0% { opacity: 0;