parent
fb0f9711ba
commit
7fc8d2e6d5
@ -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
|
||||||
|
@ -972,6 +972,8 @@ _role:
|
|||||||
noteEachClipsMax: "クリップ内のノートの最大数"
|
noteEachClipsMax: "クリップ内のノートの最大数"
|
||||||
userListMax: "ユーザーリストの作成可能数"
|
userListMax: "ユーザーリストの作成可能数"
|
||||||
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
|
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
|
||||||
|
rateLimitFactor: "レートリミット"
|
||||||
|
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
isRemote: "リモートユーザー"
|
isRemote: "リモートユーザー"
|
||||||
|
@ -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')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user