diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c9ee11e..3aba40aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ You should also include the user name that made the change. - Push notification of Antenna note @tamaina - AVIF support @tamaina - Add Cloudflare Turnstile CAPTCHA support @CyberRex0 +- レートリミットをユーザーごとに調整可能に @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo - クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ea3cc6c9e..a0802dd68 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -972,6 +972,8 @@ _role: noteEachClipsMax: "クリップ内のノートの最大数" userListMax: "ユーザーリストの作成可能数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" + rateLimitFactor: "レートリミット" + descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 13fbfaf41..9fd612c96 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -28,6 +28,7 @@ export type RoleOptions = { noteEachClipsLimit: number; userListLimit: number; userEachUserListsLimit: number; + rateLimitFactor: number; }; export const DEFAULT_ROLE: RoleOptions = { @@ -45,6 +46,7 @@ export const DEFAULT_ROLE: RoleOptions = { noteEachClipsLimit: 200, userListLimit: 10, userEachUserListsLimit: 50, + rateLimitFactor: 1, }; @Injectable() @@ -221,6 +223,7 @@ export class RoleService implements OnApplicationShutdown { noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')), userListLimit: Math.max(...getOptionValues('userListLimit')), userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')), + rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')), }; } diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index c19e861a5..dcc9342a8 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -224,8 +224,11 @@ export class ApiCallService implements OnApplicationShutdown { limit.key = ep.name; } + // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい + const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1; + // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(err => { + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { throw new ApiError({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index c893b60ba..a9c34e363 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -26,7 +26,7 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) { + public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { return new Promise((ok, reject) => { if (this.disabled) ok(); @@ -34,7 +34,7 @@ export class RateLimiterService { const min = (): void => { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval, + duration: limitation.minInterval * factor, max: 1, db: this.redisClient, }); @@ -62,8 +62,8 @@ export class RateLimiterService { const max = (): void => { const limiter = new Limiter({ id: `${actor}:${limitation.key}`, - duration: limitation.duration, - max: limitation.max, + duration: limitation.duration * factor, + max: limitation.max / factor, db: this.redisClient, }); diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index fc5e2b9d1..bd166c9d4 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -38,6 +38,19 @@
+ + + +
+ + + + + + +
+
+ @@ -241,9 +254,11 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkButton from '@/components/MkButton.vue'; +import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; +import { instance } from '@/instance'; const emit = defineEmits<{ (ev: 'created', payload: any): void; @@ -266,33 +281,35 @@ let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' }); let isPublic = $ref(role?.isPublic ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true); -let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? false); +let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? instance.baseRole.gtlAvailable); let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true); -let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? false); +let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? instance.baseRole.ltlAvailable); let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true); -let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? false); +let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? instance.baseRole.canPublicNote); let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true); -let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? false); +let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? instance.baseRole.canInvite); let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true); -let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? false); +let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? instance.baseRole.canManageCustomEmojis); let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true); -let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0); +let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? instance.baseRole.driveCapacityMb); let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true); -let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? 0); +let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? instance.baseRole.pinLimit); let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true); -let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0); +let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? instance.baseRole.antennaLimit); let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true); -let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0); +let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? instance.baseRole.wordMuteLimit); let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true); -let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0); +let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? instance.baseRole.webhookLimit); let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true); -let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? 0); +let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? instance.baseRole.clipLimit); let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true); -let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? 0); +let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? instance.baseRole.noteEachClipsLimit); let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true); -let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? 0); +let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? instance.baseRole.userListLimit); let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true); -let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? 0); +let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? instance.baseRole.userEachUserListsLimit); +let options_rateLimitFactor_useDefault = $ref(role?.options?.rateLimitFactor?.useDefault ?? true); +let options_rateLimitFactor_value = $ref(role?.options?.rateLimitFactor?.value ?? instance.baseRole.rateLimitFactor); if (_DEV_) { watch($$(condFormula), () => { @@ -316,6 +333,7 @@ function getOptions() { noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value }, userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value }, userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value }, + rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value }, }; } diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 68be6bbdd..1ceead8b6 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -8,6 +8,14 @@
+ + + + + + + + @@ -134,6 +142,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkButton from '@/components/MkButton.vue'; +import MkRange from '@/components/MkRange.vue'; import MkRolePreview from '@/components/MkRolePreview.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -159,6 +168,7 @@ let options_clipLimit = $ref(instance.baseRole.clipLimit); let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit); let options_userListLimit = $ref(instance.baseRole.userListLimit); let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit); +let options_rateLimitFactor = $ref(instance.baseRole.rateLimitFactor); async function updateBaseRole() { await os.apiWithDialog('admin/roles/update-default-role-override', { @@ -177,6 +187,7 @@ async function updateBaseRole() { noteEachClipsLimit: options_noteEachClipsLimit, userListLimit: options_userListLimit, userEachUserListsLimit: options_userEachUserListsLimit, + rateLimitFactor: options_rateLimitFactor, }, }); }