summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--locales/ja-JP.yml2
-rw-r--r--packages/backend/src/core/RoleService.ts3
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts5
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts8
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue46
-rw-r--r--packages/frontend/src/pages/admin/roles.vue11
7 files changed, 57 insertions, 19 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06c9ee11e1..3aba40aef7 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 ea3cc6c9e7..a0802dd68c 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 13fbfaf418..9fd612c96e 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 c19e861a5a..dcc9342a82 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<string> }, limitActor).catch(err => {
+ await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, 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 c893b60baf..a9c34e363a 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<string> }, actor: string) {
+ public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((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 fc5e2b9d12..bd166c9d48 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -39,6 +39,19 @@
<template #label>{{ i18n.ts._role.options }}</template>
<div class="_gaps_s">
<MkFolder>
+ <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
+ <template #suffix>{{ options_rateLimitFactor_useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options_rateLimitFactor_value * 100)}%` }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_rateLimitFactor_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkRange :model-value="options_rateLimitFactor_value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor_value = (v / 100)">
+ <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
+ </MkRange>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
<div class="_gaps">
@@ -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 68be6bbdd6..1ceead8b6c 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -9,6 +9,14 @@
<template #label>{{ i18n.ts._role.baseRole }}</template>
<div class="_gaps">
<MkFolder>
+ <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
+ <template #suffix>{{ Math.floor(options_rateLimitFactor * 100) }}%</template>
+ <MkRange :model-value="options_rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor = (v / 100)">
+ <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
+ </MkRange>
+ </MkFolder>
+
+ <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_gtlAvailable">
@@ -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,
},
});
}