Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 (#13463)
* コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 * コメント修正
This commit is contained in:
		
							parent
							
								
									0fb7b98f96
								
							
						
					
					
						commit
						f906ad6ca7
					
				
					 12 changed files with 97 additions and 9 deletions
				
			
		| 
						 | 
				
			
			@ -15,6 +15,7 @@
 | 
			
		|||
 | 
			
		||||
### General
 | 
			
		||||
- Enhance: サーバーごとにモデレーションノートを残せるように
 | 
			
		||||
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -6528,6 +6528,10 @@ export interface Locale extends ILocale {
 | 
			
		|||
            "avatarDecorationLimit": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_condition": {
 | 
			
		||||
            /**
 | 
			
		||||
             * マニュアルロールにアサイン済み
 | 
			
		||||
             */
 | 
			
		||||
            "roleAssignedTo": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * ローカルユーザー
 | 
			
		||||
             */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1687,6 +1687,7 @@ _role:
 | 
			
		|||
    canUseTranslator: "翻訳機能の利用"
 | 
			
		||||
    avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
 | 
			
		||||
  _condition:
 | 
			
		||||
    roleAssignedTo: "マニュアルロールにアサイン済み"
 | 
			
		||||
    isLocal: "ローカルユーザー"
 | 
			
		||||
    isRemote: "リモートユーザー"
 | 
			
		||||
    createdLessThan: "アカウント作成から~以内"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -200,17 +200,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean {
 | 
			
		||||
	private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
 | 
			
		||||
		try {
 | 
			
		||||
			switch (value.type) {
 | 
			
		||||
				case 'and': {
 | 
			
		||||
					return value.values.every(v => this.evalCond(user, v));
 | 
			
		||||
					return value.values.every(v => this.evalCond(user, roles, v));
 | 
			
		||||
				}
 | 
			
		||||
				case 'or': {
 | 
			
		||||
					return value.values.some(v => this.evalCond(user, v));
 | 
			
		||||
					return value.values.some(v => this.evalCond(user, roles, v));
 | 
			
		||||
				}
 | 
			
		||||
				case 'not': {
 | 
			
		||||
					return !this.evalCond(user, value.value);
 | 
			
		||||
					return !this.evalCond(user, roles, value.value);
 | 
			
		||||
				}
 | 
			
		||||
				case 'roleAssignedTo': {
 | 
			
		||||
					return roles.some(r => r.id === value.roleId);
 | 
			
		||||
				}
 | 
			
		||||
				case 'isLocal': {
 | 
			
		||||
					return this.userEntityService.isLocalUser(user);
 | 
			
		||||
| 
						 | 
				
			
			@ -272,7 +275,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		|||
		const assigns = await this.getUserAssigns(userId);
 | 
			
		||||
		const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
 | 
			
		||||
		const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
 | 
			
		||||
		const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
 | 
			
		||||
		const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
 | 
			
		||||
		return [...assignedRoles, ...matchedCondRoles];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -285,13 +288,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		|||
		let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
 | 
			
		||||
		// 期限切れのロールを除外
 | 
			
		||||
		assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
 | 
			
		||||
		const assignedRoleIds = assigns.map(x => x.roleId);
 | 
			
		||||
		const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
 | 
			
		||||
		const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
 | 
			
		||||
		const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
 | 
			
		||||
		const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
 | 
			
		||||
		const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
 | 
			
		||||
		if (badgeCondRoles.length > 0) {
 | 
			
		||||
			const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
 | 
			
		||||
			const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
 | 
			
		||||
			const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
 | 
			
		||||
			return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
 | 
			
		||||
		} else {
 | 
			
		||||
			return assignedBadgeRoles;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ import {
 | 
			
		|||
	packedRoleCondFormulaLogicsSchema,
 | 
			
		||||
	packedRoleCondFormulaValueNot,
 | 
			
		||||
	packedRoleCondFormulaValueIsLocalOrRemoteSchema,
 | 
			
		||||
	packedRoleCondFormulaValueAssignedRoleSchema,
 | 
			
		||||
	packedRoleCondFormulaValueCreatedSchema,
 | 
			
		||||
	packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
 | 
			
		||||
	packedRoleCondFormulaValueSchema,
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +97,7 @@ export const refs = {
 | 
			
		|||
	RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
 | 
			
		||||
	RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
 | 
			
		||||
	RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
 | 
			
		||||
	RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
 | 
			
		||||
	RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
 | 
			
		||||
	RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
 | 
			
		||||
	RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,11 @@ type CondFormulaValueIsRemote = {
 | 
			
		|||
	type: 'isRemote';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type CondFormulaValueRoleAssignedTo = {
 | 
			
		||||
	type: 'roleAssignedTo';
 | 
			
		||||
	roleId: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type CondFormulaValueCreatedLessThan = {
 | 
			
		||||
	type: 'createdLessThan';
 | 
			
		||||
	sec: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +80,7 @@ export type RoleCondFormulaValue = { id: string } & (
 | 
			
		|||
	CondFormulaValueNot |
 | 
			
		||||
	CondFormulaValueIsLocal |
 | 
			
		||||
	CondFormulaValueIsRemote |
 | 
			
		||||
	CondFormulaValueRoleAssignedTo |
 | 
			
		||||
	CondFormulaValueCreatedLessThan |
 | 
			
		||||
	CondFormulaValueCreatedMoreThan |
 | 
			
		||||
	CondFormulaValueFollowersLessThanOrEq |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,6 +57,23 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
 | 
			
		|||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const packedRoleCondFormulaValueAssignedRoleSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		type: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			enum: ['roleAssignedTo'],
 | 
			
		||||
		},
 | 
			
		||||
		roleId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const packedRoleCondFormulaValueCreatedSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +132,9 @@ export const packedRoleCondFormulaValueSchema = {
 | 
			
		|||
		{
 | 
			
		||||
			ref: 'RoleCondFormulaValueIsLocalOrRemote',
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			ref: 'RoleCondFormulaValueAssignedRole',
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			ref: 'RoleCondFormulaValueCreated',
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -251,6 +251,34 @@ describe('RoleService', () => {
 | 
			
		|||
			expect(user2Policies.canManageCustomEmojis).toBe(true);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('コンディショナルロール: マニュアルロールにアサイン済み', async () => {
 | 
			
		||||
			const [user1, user2, role1] = await Promise.all([
 | 
			
		||||
				createUser(),
 | 
			
		||||
				createUser(),
 | 
			
		||||
				createRole({
 | 
			
		||||
					name: 'manual role',
 | 
			
		||||
				}),
 | 
			
		||||
			]);
 | 
			
		||||
			const role2 = await createRole({
 | 
			
		||||
				name: 'conditional role',
 | 
			
		||||
				target: 'conditional',
 | 
			
		||||
				condFormula: {
 | 
			
		||||
					// idはバックエンドのロジックに必要ない?
 | 
			
		||||
					id: 'bdc612bd-9d54-4675-ae83-0499c82ea670',
 | 
			
		||||
					type: 'roleAssignedTo',
 | 
			
		||||
					roleId: role1.id,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
			await roleService.assign(user2.id, role1.id);
 | 
			
		||||
 | 
			
		||||
			const [u1role, u2role] = await Promise.all([
 | 
			
		||||
				roleService.getUserRoles(user1.id),
 | 
			
		||||
				roleService.getUserRoles(user2.id),
 | 
			
		||||
			]);
 | 
			
		||||
			expect(u1role.some(r => r.id === role2.id)).toBe(false);
 | 
			
		||||
			expect(u2role.some(r => r.id === role2.id)).toBe(true);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('expired role', async () => {
 | 
			
		||||
			const user = await createUser();
 | 
			
		||||
			const role = await createRole({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<MkSelect v-model="type" :class="$style.typeSelect">
 | 
			
		||||
			<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
 | 
			
		||||
			<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
 | 
			
		||||
			<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
 | 
			
		||||
			<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
 | 
			
		||||
			<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
 | 
			
		||||
			<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +52,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
	<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
 | 
			
		||||
	</MkInput>
 | 
			
		||||
 | 
			
		||||
	<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
 | 
			
		||||
		<option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option>
 | 
			
		||||
	</MkSelect>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +67,7 @@ import MkSelect from '@/components/MkSelect.vue';
 | 
			
		|||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { deepClone } from '@/scripts/clone.js';
 | 
			
		||||
import { rolesCache } from '@/cache.js';
 | 
			
		||||
 | 
			
		||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +83,8 @@ const props = defineProps<{
 | 
			
		|||
 | 
			
		||||
const v = ref(deepClone(props.modelValue));
 | 
			
		||||
 | 
			
		||||
const roles = await rolesCache.fetch();
 | 
			
		||||
 | 
			
		||||
watch(() => props.modelValue, () => {
 | 
			
		||||
	if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
 | 
			
		||||
	v.value = deepClone(props.modelValue);
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +100,7 @@ const type = computed({
 | 
			
		|||
		if (t === 'and') v.value.values = [];
 | 
			
		||||
		if (t === 'or') v.value.values = [];
 | 
			
		||||
		if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
 | 
			
		||||
		if (t === 'roleAssignedTo') v.value.roleId = '';
 | 
			
		||||
		if (t === 'createdLessThan') v.value.sec = 86400;
 | 
			
		||||
		if (t === 'createdMoreThan') v.value.sec = 86400;
 | 
			
		||||
		if (t === 'followersLessThanOrEq') v.value.value = 10;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1712,6 +1712,7 @@ declare namespace entities {
 | 
			
		|||
        RoleCondFormulaLogics,
 | 
			
		||||
        RoleCondFormulaValueNot,
 | 
			
		||||
        RoleCondFormulaValueIsLocalOrRemote,
 | 
			
		||||
        RoleCondFormulaValueAssignedRole,
 | 
			
		||||
        RoleCondFormulaValueCreated,
 | 
			
		||||
        RoleCondFormulaFollowersOrFollowingOrNotes,
 | 
			
		||||
        RoleCondFormulaValue,
 | 
			
		||||
| 
						 | 
				
			
			@ -2731,6 +2732,9 @@ type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
 | 
			
		|||
// @public (undocumented)
 | 
			
		||||
type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ export type Signin = components['schemas']['Signin'];
 | 
			
		|||
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
 | 
			
		||||
export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
 | 
			
		||||
export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote'];
 | 
			
		||||
export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
 | 
			
		||||
export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
 | 
			
		||||
export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
 | 
			
		||||
export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4573,6 +4573,15 @@ export type components = {
 | 
			
		|||
      /** @enum {string} */
 | 
			
		||||
      type: 'isLocal' | 'isRemote';
 | 
			
		||||
    };
 | 
			
		||||
    RoleCondFormulaValueAssignedRole: {
 | 
			
		||||
      /** @enum {string} */
 | 
			
		||||
      type: 'roleAssignedTo';
 | 
			
		||||
      /**
 | 
			
		||||
       * Format: id
 | 
			
		||||
       * @example xxxxxxxxxx
 | 
			
		||||
       */
 | 
			
		||||
      roleId: string;
 | 
			
		||||
    };
 | 
			
		||||
    RoleCondFormulaValueCreated: {
 | 
			
		||||
      id: string;
 | 
			
		||||
      /** @enum {string} */
 | 
			
		||||
| 
						 | 
				
			
			@ -4585,7 +4594,7 @@ export type components = {
 | 
			
		|||
      type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq';
 | 
			
		||||
      value: number;
 | 
			
		||||
    };
 | 
			
		||||
    RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
 | 
			
		||||
    RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
 | 
			
		||||
    RoleLite: {
 | 
			
		||||
      /**
 | 
			
		||||
       * Format: id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue