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 | - 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue