ロールでレートリミットを調整できるように

Resolve #9584
This commit is contained in:
syuilo 2023-01-15 16:52:12 +09:00
parent fb0f9711ba
commit 7fc8d2e6d5
7 changed files with 57 additions and 19 deletions

View file

@ -74,6 +74,7 @@ You should also include the user name that made the change.
- Push notification of Antenna note @tamaina - Push notification of Antenna note @tamaina
- AVIF support @tamaina - AVIF support @tamaina
- Add Cloudflare Turnstile CAPTCHA support @CyberRex0 - Add Cloudflare Turnstile CAPTCHA support @CyberRex0
- レートリミットをユーザーごとに調整可能に @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
- クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo - クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo

View file

@ -972,6 +972,8 @@ _role:
noteEachClipsMax: "クリップ内のノートの最大数" noteEachClipsMax: "クリップ内のノートの最大数"
userListMax: "ユーザーリストの作成可能数" userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"

View file

@ -28,6 +28,7 @@ export type RoleOptions = {
noteEachClipsLimit: number; noteEachClipsLimit: number;
userListLimit: number; userListLimit: number;
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number;
}; };
export const DEFAULT_ROLE: RoleOptions = { export const DEFAULT_ROLE: RoleOptions = {
@ -45,6 +46,7 @@ export const DEFAULT_ROLE: RoleOptions = {
noteEachClipsLimit: 200, noteEachClipsLimit: 200,
userListLimit: 10, userListLimit: 10,
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1,
}; };
@Injectable() @Injectable()
@ -221,6 +223,7 @@ export class RoleService implements OnApplicationShutdown {
noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')), noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')),
userListLimit: Math.max(...getOptionValues('userListLimit')), userListLimit: Math.max(...getOptionValues('userListLimit')),
userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')), userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')),
rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')),
}; };
} }

View file

@ -224,8 +224,11 @@ export class ApiCallService implements OnApplicationShutdown {
limit.key = ep.name; limit.key = ep.name;
} }
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1;
// Rate limit // 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({ throw new ApiError({
message: 'Rate limit exceeded. Please try again later.', message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED', code: 'RATE_LIMIT_EXCEEDED',

View file

@ -26,7 +26,7 @@ export class RateLimiterService {
} }
@bindThis @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) => { return new Promise<void>((ok, reject) => {
if (this.disabled) ok(); if (this.disabled) ok();
@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => { const min = (): void => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval, duration: limitation.minInterval * factor,
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration, duration: limitation.duration * factor,
max: limitation.max, max: limitation.max / factor,
db: this.redisClient, db: this.redisClient,
}); });

View file

@ -38,6 +38,19 @@
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts._role.options }}</template> <template #label>{{ i18n.ts._role.options }}</template>
<div class="_gaps_s"> <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> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <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> <template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
@ -241,9 +254,11 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { instance } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'created', payload: any): void; (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 isPublic = $ref(role?.isPublic ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true); 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_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_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_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_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_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_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_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_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_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_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_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_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_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_) { if (_DEV_) {
watch($$(condFormula), () => { watch($$(condFormula), () => {
@ -316,6 +333,7 @@ function getOptions() {
noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value }, noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value }, userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value }, userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value },
}; };
} }

View file

@ -8,6 +8,14 @@
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role.baseRole }}</template> <template #label>{{ i18n.ts._role.baseRole }}</template>
<div class="_gaps"> <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> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
@ -134,6 +142,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -159,6 +168,7 @@ let options_clipLimit = $ref(instance.baseRole.clipLimit);
let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit); let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit);
let options_userListLimit = $ref(instance.baseRole.userListLimit); let options_userListLimit = $ref(instance.baseRole.userListLimit);
let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit); let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor = $ref(instance.baseRole.rateLimitFactor);
async function updateBaseRole() { async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-role-override', { await os.apiWithDialog('admin/roles/update-default-role-override', {
@ -177,6 +187,7 @@ async function updateBaseRole() {
noteEachClipsLimit: options_noteEachClipsLimit, noteEachClipsLimit: options_noteEachClipsLimit,
userListLimit: options_userListLimit, userListLimit: options_userListLimit,
userEachUserListsLimit: options_userEachUserListsLimit, userEachUserListsLimit: options_userEachUserListsLimit,
rateLimitFactor: options_rateLimitFactor,
}, },
}); });
} }