Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 (#13463)

* コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加

* コメント修正
This commit is contained in:
zawa-ch 2024-02-27 18:45:46 +09:00 committed by GitHub
parent 0fb7b98f96
commit f906ad6ca7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 97 additions and 9 deletions

View file

@ -15,6 +15,7 @@
### General ### General
- Enhance: サーバーごとにモデレーションノートを残せるように - Enhance: サーバーごとにモデレーションノートを残せるように
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
### Client ### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整

4
locales/index.d.ts vendored
View file

@ -6528,6 +6528,10 @@ export interface Locale extends ILocale {
"avatarDecorationLimit": string; "avatarDecorationLimit": string;
}; };
"_condition": { "_condition": {
/**
*
*/
"roleAssignedTo": string;
/** /**
* *
*/ */

View file

@ -1687,6 +1687,7 @@ _role:
canUseTranslator: "翻訳機能の利用" canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数" avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
createdLessThan: "アカウント作成から~以内" createdLessThan: "アカウント作成から~以内"

View file

@ -200,17 +200,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean { private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
try { try {
switch (value.type) { switch (value.type) {
case 'and': { case 'and': {
return value.values.every(v => this.evalCond(user, v)); return value.values.every(v => this.evalCond(user, roles, v));
} }
case 'or': { case 'or': {
return value.values.some(v => this.evalCond(user, v)); return value.values.some(v => this.evalCond(user, roles, v));
} }
case 'not': { 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': { case 'isLocal': {
return this.userEntityService.isLocalUser(user); return this.userEntityService.isLocalUser(user);
@ -272,7 +275,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
const assigns = await this.getUserAssigns(userId); const assigns = await this.getUserAssigns(userId);
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); 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 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]; return [...assignedRoles, ...matchedCondRoles];
} }
@ -285,13 +288,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); 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 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')); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) { if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; 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]; return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else { } else {
return assignedBadgeRoles; return assignedBadgeRoles;

View file

@ -44,6 +44,7 @@ import {
packedRoleCondFormulaLogicsSchema, packedRoleCondFormulaLogicsSchema,
packedRoleCondFormulaValueNot, packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueIsLocalOrRemoteSchema, packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueAssignedRoleSchema,
packedRoleCondFormulaValueCreatedSchema, packedRoleCondFormulaValueCreatedSchema,
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaValueSchema, packedRoleCondFormulaValueSchema,
@ -96,6 +97,7 @@ export const refs = {
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
RoleCondFormulaValue: packedRoleCondFormulaValueSchema, RoleCondFormulaValue: packedRoleCondFormulaValueSchema,

View file

@ -29,6 +29,11 @@ type CondFormulaValueIsRemote = {
type: 'isRemote'; type: 'isRemote';
}; };
type CondFormulaValueRoleAssignedTo = {
type: 'roleAssignedTo';
roleId: string;
};
type CondFormulaValueCreatedLessThan = { type CondFormulaValueCreatedLessThan = {
type: 'createdLessThan'; type: 'createdLessThan';
sec: number; sec: number;
@ -75,6 +80,7 @@ export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueNot | CondFormulaValueNot |
CondFormulaValueIsLocal | CondFormulaValueIsLocal |
CondFormulaValueIsRemote | CondFormulaValueIsRemote |
CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan | CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan | CondFormulaValueCreatedMoreThan |
CondFormulaValueFollowersLessThanOrEq | CondFormulaValueFollowersLessThanOrEq |

View file

@ -57,6 +57,23 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
}, },
} as const; } 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 = { export const packedRoleCondFormulaValueCreatedSchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -115,6 +132,9 @@ export const packedRoleCondFormulaValueSchema = {
{ {
ref: 'RoleCondFormulaValueIsLocalOrRemote', ref: 'RoleCondFormulaValueIsLocalOrRemote',
}, },
{
ref: 'RoleCondFormulaValueAssignedRole',
},
{ {
ref: 'RoleCondFormulaValueCreated', ref: 'RoleCondFormulaValueCreated',
}, },

View file

@ -251,6 +251,34 @@ describe('RoleService', () => {
expect(user2Policies.canManageCustomEmojis).toBe(true); 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 () => { test('expired role', async () => {
const user = await createUser(); const user = await createUser();
const role = await createRole({ const role = await createRole({

View file

@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="type" :class="$style.typeSelect"> <MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option> <option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</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="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option> <option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</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 v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
</MkInput> </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> </div>
</template> </template>
@ -62,6 +67,7 @@ import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { rolesCache } from '@/cache.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -77,6 +83,8 @@ const props = defineProps<{
const v = ref(deepClone(props.modelValue)); const v = ref(deepClone(props.modelValue));
const roles = await rolesCache.fetch();
watch(() => props.modelValue, () => { watch(() => props.modelValue, () => {
if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return; if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
v.value = deepClone(props.modelValue); v.value = deepClone(props.modelValue);
@ -92,6 +100,7 @@ const type = computed({
if (t === 'and') v.value.values = []; if (t === 'and') v.value.values = [];
if (t === 'or') v.value.values = []; if (t === 'or') v.value.values = [];
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' }; 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 === 'createdLessThan') v.value.sec = 86400;
if (t === 'createdMoreThan') v.value.sec = 86400; if (t === 'createdMoreThan') v.value.sec = 86400;
if (t === 'followersLessThanOrEq') v.value.value = 10; if (t === 'followersLessThanOrEq') v.value.value = 10;

View file

@ -1712,6 +1712,7 @@ declare namespace entities {
RoleCondFormulaLogics, RoleCondFormulaLogics,
RoleCondFormulaValueNot, RoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote, RoleCondFormulaValueIsLocalOrRemote,
RoleCondFormulaValueAssignedRole,
RoleCondFormulaValueCreated, RoleCondFormulaValueCreated,
RoleCondFormulaFollowersOrFollowingOrNotes, RoleCondFormulaFollowersOrFollowingOrNotes,
RoleCondFormulaValue, RoleCondFormulaValue,
@ -2731,6 +2732,9 @@ type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
// @public (undocumented) // @public (undocumented)
type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
// @public (undocumented)
type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
// @public (undocumented) // @public (undocumented)
type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];

View file

@ -38,6 +38,7 @@ export type Signin = components['schemas']['Signin'];
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote'];
export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];

View file

@ -4573,6 +4573,15 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
type: 'isLocal' | 'isRemote'; type: 'isLocal' | 'isRemote';
}; };
RoleCondFormulaValueAssignedRole: {
/** @enum {string} */
type: 'roleAssignedTo';
/**
* Format: id
* @example xxxxxxxxxx
*/
roleId: string;
};
RoleCondFormulaValueCreated: { RoleCondFormulaValueCreated: {
id: string; id: string;
/** @enum {string} */ /** @enum {string} */
@ -4585,7 +4594,7 @@ export type components = {
type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq'; type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq';
value: number; 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: { RoleLite: {
/** /**
* Format: id * Format: id