parent
							
								
									fb0f9711ba
								
							
						
					
					
						commit
						7fc8d2e6d5
					
				
					 7 changed files with 57 additions and 19 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -972,6 +972,8 @@ _role: | |||
|     noteEachClipsMax: "クリップ内のノートの最大数" | ||||
|     userListMax: "ユーザーリストの作成可能数" | ||||
|     userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" | ||||
|     rateLimitFactor: "レートリミット" | ||||
|     descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" | ||||
|   _condition: | ||||
|     isLocal: "ローカルユーザー" | ||||
|     isRemote: "リモートユーザー" | ||||
|  |  | |||
|  | @ -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')), | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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, | ||||
| 				}); | ||||
| 		 | ||||
|  |  | |||
|  | @ -38,6 +38,19 @@ | |||
| 	<FormSlot> | ||||
| 		<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> | ||||
|  | @ -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 }, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,14 @@ | |||
| 				<MkFolder> | ||||
| 					<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> | ||||
|  | @ -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, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue