enhance: 新しいコンディショナルロール条件の実装 (#13732)

* enhance: 新しいコンディショナルロールの実装

* fix: CHANGELOG.md
This commit is contained in:
おさむのひと 2024-04-19 15:22:23 +09:00 committed by GitHub
parent ea9aa6fdb4
commit cd7f7271ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 624 additions and 74 deletions

View file

@ -8,6 +8,12 @@
- Enhance: アンテナでBotによるートを除外できるように - Enhance: アンテナでBotによるートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
- Enhance: クリップのノート数を表示するように - Enhance: クリップのノート数を表示するように
- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667)
- 猫ユーザーか
- botユーザーか
- サスペンド済みユーザーか
- 鍵アカウントユーザーか
- 「アカウントを見つけやすくする」が有効なユーザーか
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 - Fix: Play作成時に設定した公開範囲が機能していない問題を修正
### Client ### Client

20
locales/index.d.ts vendored
View file

@ -6592,6 +6592,26 @@ export interface Locale extends ILocale {
* *
*/ */
"isRemote": string; "isRemote": string;
/**
*
*/
"isCat": string;
/**
* botユーザー
*/
"isBot": string;
/**
*
*/
"isSuspended": string;
/**
*
*/
"isLocked": string;
/**
*
*/
"isExplorable": string;
/** /**
* *
*/ */

View file

@ -1703,6 +1703,11 @@ _role:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
isCat: "猫ユーザー"
isBot: "botユーザー"
isSuspended: "サスペンド済みユーザー"
isLocked: "鍵アカウントユーザー"
isExplorable: "「アカウントを見つけやすくする」が有効なユーザー"
createdLessThan: "アカウント作成から~以内" createdLessThan: "アカウント作成から~以内"
createdMoreThan: "アカウント作成から~経過" createdMoreThan: "アカウント作成から~経過"
followersLessThanOrEq: "フォロワー数が~以下" followersLessThanOrEq: "フォロワー数が~以下"

View file

@ -205,45 +205,79 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private evalCond(user: MiUser, roles: MiRole[], 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, roles, v)); return value.values.every(v => this.evalCond(user, roles, v));
} }
// ~または~
case 'or': { case 'or': {
return value.values.some(v => this.evalCond(user, roles, v)); return value.values.some(v => this.evalCond(user, roles, v));
} }
// ~ではない
case 'not': { case 'not': {
return !this.evalCond(user, roles, value.value); return !this.evalCond(user, roles, value.value);
} }
// マニュアルロールがアサインされている
case 'roleAssignedTo': { case 'roleAssignedTo': {
return roles.some(r => r.id === value.roleId); return roles.some(r => r.id === value.roleId);
} }
// ローカルユーザのみ
case 'isLocal': { case 'isLocal': {
return this.userEntityService.isLocalUser(user); return this.userEntityService.isLocalUser(user);
} }
// リモートユーザのみ
case 'isRemote': { case 'isRemote': {
return this.userEntityService.isRemoteUser(user); return this.userEntityService.isRemoteUser(user);
} }
// サスペンド済みユーザである
case 'isSuspended': {
return user.isSuspended;
}
// 鍵アカウントユーザである
case 'isLocked': {
return user.isLocked;
}
// botユーザである
case 'isBot': {
return user.isBot;
}
// 猫である
case 'isCat': {
return user.isCat;
}
// 「ユーザを見つけやすくする」が有効なアカウント
case 'isExplorable': {
return user.isExplorable;
}
// ユーザが作成されてから指定期間経過した
case 'createdLessThan': { case 'createdLessThan': {
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000)); return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
} }
// ユーザが作成されてから指定期間経っていない
case 'createdMoreThan': { case 'createdMoreThan': {
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000)); return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
} }
// フォロワー数が指定値以下
case 'followersLessThanOrEq': { case 'followersLessThanOrEq': {
return user.followersCount <= value.value; return user.followersCount <= value.value;
} }
// フォロワー数が指定値以上
case 'followersMoreThanOrEq': { case 'followersMoreThanOrEq': {
return user.followersCount >= value.value; return user.followersCount >= value.value;
} }
// フォロー数が指定値以下
case 'followingLessThanOrEq': { case 'followingLessThanOrEq': {
return user.followingCount <= value.value; return user.followingCount <= value.value;
} }
// フォロー数が指定値以上
case 'followingMoreThanOrEq': { case 'followingMoreThanOrEq': {
return user.followingCount >= value.value; return user.followingCount >= value.value;
} }
// ノート数が指定値以下
case 'notesLessThanOrEq': { case 'notesLessThanOrEq': {
return user.notesCount <= value.value; return user.notesCount <= value.value;
} }
// ノート数が指定値以上
case 'notesMoreThanOrEq': { case 'notesMoreThanOrEq': {
return user.notesCount >= value.value; return user.notesCount >= value.value;
} }

View file

@ -48,6 +48,7 @@ import {
packedRoleCondFormulaValueCreatedSchema, packedRoleCondFormulaValueCreatedSchema,
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaValueSchema, packedRoleCondFormulaValueSchema,
packedRoleCondFormulaValueUserSettingBooleanSchema,
} from '@/models/json-schema/role.js'; } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js'; import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
@ -97,6 +98,7 @@ export const refs = {
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema,
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema, RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,

View file

@ -6,69 +6,149 @@
import { Entity, Column, PrimaryColumn } from 'typeorm'; import { Entity, Column, PrimaryColumn } from 'typeorm';
import { id } from './util/id.js'; import { id } from './util/id.js';
/**
*
*
*/
type CondFormulaValueAnd = { type CondFormulaValueAnd = {
type: 'and'; type: 'and';
values: RoleCondFormulaValue[]; values: RoleCondFormulaValue[];
}; };
/**
*
*
*/
type CondFormulaValueOr = { type CondFormulaValueOr = {
type: 'or'; type: 'or';
values: RoleCondFormulaValue[]; values: RoleCondFormulaValue[];
}; };
/**
*
*
*/
type CondFormulaValueNot = { type CondFormulaValueNot = {
type: 'not'; type: 'not';
value: RoleCondFormulaValue; value: RoleCondFormulaValue;
}; };
/**
*
*/
type CondFormulaValueIsLocal = { type CondFormulaValueIsLocal = {
type: 'isLocal'; type: 'isLocal';
}; };
/**
*
*/
type CondFormulaValueIsRemote = { type CondFormulaValueIsRemote = {
type: 'isRemote'; type: 'isRemote';
}; };
/**
*
*/
type CondFormulaValueRoleAssignedTo = { type CondFormulaValueRoleAssignedTo = {
type: 'roleAssignedTo'; type: 'roleAssignedTo';
roleId: string; roleId: string;
}; };
/**
*
*/
type CondFormulaValueIsSuspended = {
type: 'isSuspended';
};
/**
*
*/
type CondFormulaValueIsLocked = {
type: 'isLocked';
};
/**
* botアカウントの場合のみ成立とする
*/
type CondFormulaValueIsBot = {
type: 'isBot';
};
/**
*
*/
type CondFormulaValueIsCat = {
type: 'isCat';
};
/**
*
*/
type CondFormulaValueIsExplorable = {
type: 'isExplorable';
};
/**
*
*/
type CondFormulaValueCreatedLessThan = { type CondFormulaValueCreatedLessThan = {
type: 'createdLessThan'; type: 'createdLessThan';
sec: number; sec: number;
}; };
/**
*
*/
type CondFormulaValueCreatedMoreThan = { type CondFormulaValueCreatedMoreThan = {
type: 'createdMoreThan'; type: 'createdMoreThan';
sec: number; sec: number;
}; };
/**
*
*/
type CondFormulaValueFollowersLessThanOrEq = { type CondFormulaValueFollowersLessThanOrEq = {
type: 'followersLessThanOrEq'; type: 'followersLessThanOrEq';
value: number; value: number;
}; };
/**
*
*/
type CondFormulaValueFollowersMoreThanOrEq = { type CondFormulaValueFollowersMoreThanOrEq = {
type: 'followersMoreThanOrEq'; type: 'followersMoreThanOrEq';
value: number; value: number;
}; };
/**
*
*/
type CondFormulaValueFollowingLessThanOrEq = { type CondFormulaValueFollowingLessThanOrEq = {
type: 'followingLessThanOrEq'; type: 'followingLessThanOrEq';
value: number; value: number;
}; };
/**
*
*/
type CondFormulaValueFollowingMoreThanOrEq = { type CondFormulaValueFollowingMoreThanOrEq = {
type: 'followingMoreThanOrEq'; type: 'followingMoreThanOrEq';
value: number; value: number;
}; };
/**
* 稿
*/
type CondFormulaValueNotesLessThanOrEq = { type CondFormulaValueNotesLessThanOrEq = {
type: 'notesLessThanOrEq'; type: 'notesLessThanOrEq';
value: number; value: number;
}; };
/**
* 稿
*/
type CondFormulaValueNotesMoreThanOrEq = { type CondFormulaValueNotesMoreThanOrEq = {
type: 'notesMoreThanOrEq'; type: 'notesMoreThanOrEq';
value: number; value: number;
@ -80,6 +160,11 @@ export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueNot | CondFormulaValueNot |
CondFormulaValueIsLocal | CondFormulaValueIsLocal |
CondFormulaValueIsRemote | CondFormulaValueIsRemote |
CondFormulaValueIsSuspended |
CondFormulaValueIsLocked |
CondFormulaValueIsBot |
CondFormulaValueIsCat |
CondFormulaValueIsExplorable |
CondFormulaValueRoleAssignedTo | CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan | CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan | CondFormulaValueCreatedMoreThan |

View file

@ -57,6 +57,20 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
}, },
} as const; } as const;
export const packedRoleCondFormulaValueUserSettingBooleanSchema = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'],
},
},
} as const;
export const packedRoleCondFormulaValueAssignedRoleSchema = { export const packedRoleCondFormulaValueAssignedRoleSchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -135,6 +149,9 @@ export const packedRoleCondFormulaValueSchema = {
{ {
ref: 'RoleCondFormulaValueIsLocalOrRemote', ref: 'RoleCondFormulaValueIsLocalOrRemote',
}, },
{
ref: 'RoleCondFormulaValueUserSettingBooleanSchema',
},
{ {
ref: 'RoleCondFormulaValueAssignedRole', ref: 'RoleCondFormulaValueAssignedRole',
}, },

View file

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
@ -20,6 +22,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { sleep } from '../utils.js'; import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -52,12 +55,26 @@ describe('RoleService', () => {
id: genAidx(Date.now()), id: genAidx(Date.now()),
updatedAt: new Date(), updatedAt: new Date(),
lastUsedAt: new Date(), lastUsedAt: new Date(),
name: '',
description: '', description: '',
...data, ...data,
}) })
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); .then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
} }
function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) {
return createRole({
name: `[conditional] ${condFormula.type}`,
target: 'conditional',
condFormula: condFormula,
...data,
});
}
function aidx() {
return genAidx(Date.now());
}
beforeEach(async () => { beforeEach(async () => {
clock = lolex.install({ clock = lolex.install({
now: new Date(), now: new Date(),
@ -73,6 +90,7 @@ describe('RoleService', () => {
CacheService, CacheService,
IdService, IdService,
GlobalEventService, GlobalEventService,
UserEntityService,
{ {
provide: NotificationService, provide: NotificationService,
useFactory: () => ({ useFactory: () => ({
@ -209,79 +227,6 @@ describe('RoleService', () => {
expect(result.driveCapacityMb).toBe(100); expect(result.driveCapacityMb).toBe(100);
}); });
test('conditional role', async () => {
const user1 = await createUser({
id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)),
});
const user2 = await createUser({
id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)),
followersCount: 10,
});
await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
useDefault: false,
priority: 0,
value: true,
},
},
target: 'conditional',
condFormula: {
id: '232a4221-9816-49a6-a967-ae0fac52ec5e',
type: 'and',
values: [{
id: '2a37ef43-2d93-4c4d-87f6-f2fdb7d9b530',
type: 'followersMoreThanOrEq',
value: 10,
}, {
id: '1bd67839-b126-4f92-bad0-4e285dab453b',
type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7,
}],
},
});
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const user1Policies = await roleService.getUserPolicies(user1.id);
const user2Policies = await roleService.getUserPolicies(user2.id);
expect(user1Policies.canManageCustomEmojis).toBe(false);
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({
@ -320,6 +265,427 @@ describe('RoleService', () => {
}); });
}); });
describe('conditional role', () => {
test('~かつ~', async () => {
const [user1, user2, user3, user4] = await Promise.all([
createUser({ isBot: true, isCat: false, isSuspended: false }),
createUser({ isBot: false, isCat: true, isSuspended: false }),
createUser({ isBot: true, isCat: true, isSuspended: false }),
createUser({ isBot: false, isCat: false, isSuspended: true }),
]);
const role1 = await createConditionalRole({
id: aidx(),
type: 'isBot',
});
const role2 = await createConditionalRole({
id: aidx(),
type: 'isCat',
});
const role3 = await createConditionalRole({
id: aidx(),
type: 'isSuspended',
});
const role4 = await createConditionalRole({
id: aidx(),
type: 'and',
values: [role1.condFormula, role2.condFormula],
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
const actual4 = await roleService.getUserRoles(user4.id);
expect(actual1.some(r => r.id === role4.id)).toBe(false);
expect(actual2.some(r => r.id === role4.id)).toBe(false);
expect(actual3.some(r => r.id === role4.id)).toBe(true);
expect(actual4.some(r => r.id === role4.id)).toBe(false);
});
test('~または~', async () => {
const [user1, user2, user3, user4] = await Promise.all([
createUser({ isBot: true, isCat: false, isSuspended: false }),
createUser({ isBot: false, isCat: true, isSuspended: false }),
createUser({ isBot: true, isCat: true, isSuspended: false }),
createUser({ isBot: false, isCat: false, isSuspended: true }),
]);
const role1 = await createConditionalRole({
id: aidx(),
type: 'isBot',
});
const role2 = await createConditionalRole({
id: aidx(),
type: 'isCat',
});
const role3 = await createConditionalRole({
id: aidx(),
type: 'isSuspended',
});
const role4 = await createConditionalRole({
id: aidx(),
type: 'or',
values: [role1.condFormula, role2.condFormula],
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
const actual4 = await roleService.getUserRoles(user4.id);
expect(actual1.some(r => r.id === role4.id)).toBe(true);
expect(actual2.some(r => r.id === role4.id)).toBe(true);
expect(actual3.some(r => r.id === role4.id)).toBe(true);
expect(actual4.some(r => r.id === role4.id)).toBe(false);
});
test('~ではない', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ isBot: true, isCat: false, isSuspended: false }),
createUser({ isBot: false, isCat: true, isSuspended: false }),
createUser({ isBot: true, isCat: true, isSuspended: false }),
]);
const role1 = await createConditionalRole({
id: aidx(),
type: 'isBot',
});
const role2 = await createConditionalRole({
id: aidx(),
type: 'isCat',
});
const role4 = await createConditionalRole({
id: aidx(),
type: 'not',
value: role1.condFormula,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role4.id)).toBe(false);
expect(actual2.some(r => r.id === role4.id)).toBe(true);
expect(actual3.some(r => r.id === role4.id)).toBe(false);
});
test('マニュアルロールにアサイン済み', async () => {
const [user1, user2, role1] = await Promise.all([
createUser(),
createUser(),
createRole({
name: 'manual role',
}),
]);
const role2 = await createConditionalRole({
id: aidx(),
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('ローカルユーザのみ', async () => {
const [user1, user2] = await Promise.all([
createUser({ host: null }),
createUser({ host: 'example.com' }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isLocal',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(true);
expect(actual2.some(r => r.id === role.id)).toBe(false);
});
test('リモートユーザのみ', async () => {
const [user1, user2] = await Promise.all([
createUser({ host: null }),
createUser({ host: 'example.com' }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isRemote',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('サスペンド済みユーザである', async () => {
const [user1, user2] = await Promise.all([
createUser({ isSuspended: false }),
createUser({ isSuspended: true }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isSuspended',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('鍵アカウントユーザである', async () => {
const [user1, user2] = await Promise.all([
createUser({ isLocked: false }),
createUser({ isLocked: true }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isLocked',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('botユーザである', async () => {
const [user1, user2] = await Promise.all([
createUser({ isBot: false }),
createUser({ isBot: true }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isBot',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('猫である', async () => {
const [user1, user2] = await Promise.all([
createUser({ isCat: false }),
createUser({ isCat: true }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isCat',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('「ユーザを見つけやすくする」が有効なアカウント', async () => {
const [user1, user2] = await Promise.all([
createUser({ isExplorable: false }),
createUser({ isExplorable: true }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'isExplorable',
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
});
test('ユーザが作成されてから指定期間経過した', async () => {
const base = new Date();
base.setMinutes(base.getMinutes() - 5);
const d1 = new Date(base);
const d2 = new Date(base);
const d3 = new Date(base);
d1.setSeconds(d1.getSeconds() - 1);
d3.setSeconds(d3.getSeconds() + 1);
const [user1, user2, user3] = await Promise.all([
// 4:59
createUser({ id: genAidx(d1.getTime()) }),
// 5:00
createUser({ id: genAidx(d2.getTime()) }),
// 5:01
createUser({ id: genAidx(d3.getTime()) }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'createdLessThan',
// 5 minutes
sec: 300,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(false);
expect(actual3.some(r => r.id === role.id)).toBe(true);
});
test('ユーザが作成されてから指定期間経っていない', async () => {
const base = new Date();
base.setMinutes(base.getMinutes() - 5);
const d1 = new Date(base);
const d2 = new Date(base);
const d3 = new Date(base);
d1.setSeconds(d1.getSeconds() - 1);
d3.setSeconds(d3.getSeconds() + 1);
const [user1, user2, user3] = await Promise.all([
// 4:59
createUser({ id: genAidx(d1.getTime()) }),
// 5:00
createUser({ id: genAidx(d2.getTime()) }),
// 5:01
createUser({ id: genAidx(d3.getTime()) }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'createdMoreThan',
// 5 minutes
sec: 300,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(true);
expect(actual2.some(r => r.id === role.id)).toBe(false);
expect(actual3.some(r => r.id === role.id)).toBe(false);
});
test('フォロワー数が指定値以下', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ followersCount: 99 }),
createUser({ followersCount: 100 }),
createUser({ followersCount: 101 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'followersLessThanOrEq',
value: 100,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(true);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(false);
});
test('フォロワー数が指定値以下', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ followersCount: 99 }),
createUser({ followersCount: 100 }),
createUser({ followersCount: 101 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'followersMoreThanOrEq',
value: 100,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(true);
});
test('フォロー数が指定値以下', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ followingCount: 99 }),
createUser({ followingCount: 100 }),
createUser({ followingCount: 101 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'followingLessThanOrEq',
value: 100,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(true);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(false);
});
test('フォロー数が指定値以上', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ followingCount: 99 }),
createUser({ followingCount: 100 }),
createUser({ followingCount: 101 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'followingMoreThanOrEq',
value: 100,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(true);
});
test('ノート数が指定値以下', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ notesCount: 9 }),
createUser({ notesCount: 10 }),
createUser({ notesCount: 11 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'notesLessThanOrEq',
value: 10,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(true);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(false);
});
test('ノート数が指定値以上', async () => {
const [user1, user2, user3] = await Promise.all([
createUser({ notesCount: 9 }),
createUser({ notesCount: 10 }),
createUser({ notesCount: 11 }),
]);
const role = await createConditionalRole({
id: aidx(),
type: 'notesMoreThanOrEq',
value: 10,
});
const actual1 = await roleService.getUserRoles(user1.id);
const actual2 = await roleService.getUserRoles(user2.id);
const actual3 = await roleService.getUserRoles(user3.id);
expect(actual1.some(r => r.id === role.id)).toBe(false);
expect(actual2.some(r => r.id === role.id)).toBe(true);
expect(actual3.some(r => r.id === role.id)).toBe(true);
});
});
describe('assign', () => { describe('assign', () => {
test('公開ロールの場合は通知される', async () => { test('公開ロールの場合は通知される', async () => {
const user = await createUser(); const user = await createUser();

View file

@ -9,6 +9,11 @@ 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="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
<option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
<option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</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>

View file

@ -1713,6 +1713,7 @@ declare namespace entities {
RoleCondFormulaLogics, RoleCondFormulaLogics,
RoleCondFormulaValueNot, RoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote, RoleCondFormulaValueIsLocalOrRemote,
RoleCondFormulaValueUserSettingBooleanSchema,
RoleCondFormulaValueAssignedRole, RoleCondFormulaValueAssignedRole,
RoleCondFormulaValueCreated, RoleCondFormulaValueCreated,
RoleCondFormulaFollowersOrFollowingOrNotes, RoleCondFormulaFollowersOrFollowingOrNotes,
@ -2745,6 +2746,9 @@ type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormul
// @public (undocumented) // @public (undocumented)
type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
// @public (undocumented)
type RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'];
// @public (undocumented) // @public (undocumented)
type RoleLite = components['schemas']['RoleLite']; type RoleLite = components['schemas']['RoleLite'];

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 RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'];
export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole']; 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'];

View file

@ -4586,6 +4586,11 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
type: 'isLocal' | 'isRemote'; type: 'isLocal' | 'isRemote';
}; };
RoleCondFormulaValueUserSettingBooleanSchema: {
id: string;
/** @enum {string} */
type: 'isSuspended' | 'isLocked' | 'isBot' | 'isCat' | 'isExplorable';
};
RoleCondFormulaValueAssignedRole: { RoleCondFormulaValueAssignedRole: {
id: string; id: string;
/** @enum {string} */ /** @enum {string} */
@ -4608,7 +4613,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']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'] | components['schemas']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
RoleLite: { RoleLite: {
/** /**
* Format: id * Format: id