From ea9aa6fdb41d3d5c0611f17fdacedee4861fdd37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 17 Apr 2024 18:29:35 +0900
Subject: [PATCH 1/3] =?UTF-8?q?:art:=20=EF=BC=88=E3=83=9A=E3=83=BC?=
=?UTF-8?q?=E3=82=B8=E8=A1=A8=E7=A4=BA=E9=83=A8=E4=B8=8A=E9=83=A8=E3=81=AE?=
=?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E9=A0=86=E5=BA=8F=E3=82=92=E5=A4=89?=
=?UTF-8?q?=E6=9B=B4=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix https://github.com/misskey-dev/misskey/pull/13724#discussion_r1568179954
---
packages/frontend/src/pages/page.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 893c2deebf5c..e73d0320006c 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -47,8 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
From cd7f7271ca5595cae95f6fb0280fac9dee77d751 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
<46447427+samunohito@users.noreply.github.com>
Date: Fri, 19 Apr 2024 15:22:23 +0900
Subject: [PATCH 2/3] =?UTF-8?q?enhance:=20=E6=96=B0=E3=81=97=E3=81=84?=
=?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=87=E3=82=A3=E3=82=B7=E3=83=A7=E3=83=8A?=
=?UTF-8?q?=E3=83=AB=E3=83=AD=E3=83=BC=E3=83=AB=E6=9D=A1=E4=BB=B6=E3=81=AE?=
=?UTF-8?q?=E5=AE=9F=E8=A3=85=20(#13732)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* enhance: 新しいコンディショナルロールの実装
* fix: CHANGELOG.md
---
CHANGELOG.md | 6 +
locales/index.d.ts | 20 +
locales/ja-JP.yml | 5 +
packages/backend/src/core/RoleService.ts | 34 ++
packages/backend/src/misc/json-schema.ts | 2 +
packages/backend/src/models/Role.ts | 85 +++
.../backend/src/models/json-schema/role.ts | 17 +
packages/backend/test/unit/RoleService.ts | 498 +++++++++++++++---
.../src/pages/admin/RolesEditorFormula.vue | 5 +
packages/misskey-js/etc/misskey-js.api.md | 4 +
packages/misskey-js/src/autogen/models.ts | 1 +
packages/misskey-js/src/autogen/types.ts | 7 +-
12 files changed, 617 insertions(+), 67 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36632e5024f4..c13fa664dd53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@
- Enhance: アンテナでBotによるノートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
- Enhance: クリップのノート数を表示するように
+- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667)
+ - 猫ユーザーか
+ - botユーザーか
+ - サスペンド済みユーザーか
+ - 鍵アカウントユーザーか
+ - 「アカウントを見つけやすくする」が有効なユーザーか
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
### Client
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 8e31fc8d597c..9bcd1979afbd 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -6592,6 +6592,26 @@ export interface Locale extends ILocale {
* リモートユーザー
*/
"isRemote": string;
+ /**
+ * 猫ユーザー
+ */
+ "isCat": string;
+ /**
+ * botユーザー
+ */
+ "isBot": string;
+ /**
+ * サスペンド済みユーザー
+ */
+ "isSuspended": string;
+ /**
+ * 鍵アカウントユーザー
+ */
+ "isLocked": string;
+ /**
+ * 「アカウントを見つけやすくする」が有効なユーザー
+ */
+ "isExplorable": string;
/**
* アカウント作成から~以内
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index f5984597929a..5f7715b210ed 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1703,6 +1703,11 @@ _role:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
isRemote: "リモートユーザー"
+ isCat: "猫ユーザー"
+ isBot: "botユーザー"
+ isSuspended: "サスペンド済みユーザー"
+ isLocked: "鍵アカウントユーザー"
+ isExplorable: "「アカウントを見つけやすくする」が有効なユーザー"
createdLessThan: "アカウント作成から~以内"
createdMoreThan: "アカウント作成から~経過"
followersLessThanOrEq: "フォロワー数が~以下"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 09f309711445..70c537f9abdc 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -205,45 +205,79 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
try {
switch (value.type) {
+ // ~かつ~
case 'and': {
return value.values.every(v => this.evalCond(user, roles, v));
}
+ // ~または~
case 'or': {
return value.values.some(v => this.evalCond(user, roles, v));
}
+ // ~ではない
case 'not': {
return !this.evalCond(user, roles, value.value);
}
+ // マニュアルロールがアサインされている
case 'roleAssignedTo': {
return roles.some(r => r.id === value.roleId);
}
+ // ローカルユーザのみ
case 'isLocal': {
return this.userEntityService.isLocalUser(user);
}
+ // リモートユーザのみ
case 'isRemote': {
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': {
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
}
+ // ユーザが作成されてから指定期間経っていない
case 'createdMoreThan': {
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
}
+ // フォロワー数が指定値以下
case 'followersLessThanOrEq': {
return user.followersCount <= value.value;
}
+ // フォロワー数が指定値以上
case 'followersMoreThanOrEq': {
return user.followersCount >= value.value;
}
+ // フォロー数が指定値以下
case 'followingLessThanOrEq': {
return user.followingCount <= value.value;
}
+ // フォロー数が指定値以上
case 'followingMoreThanOrEq': {
return user.followingCount >= value.value;
}
+ // ノート数が指定値以下
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
+ // ノート数が指定値以上
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 46b0bb2fab67..a620d7c94b8c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -48,6 +48,7 @@ import {
packedRoleCondFormulaValueCreatedSchema,
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaValueSchema,
+ packedRoleCondFormulaValueUserSettingBooleanSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
@@ -97,6 +98,7 @@ export const refs = {
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema,
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts
index 058abe311863..a173971b2ce2 100644
--- a/packages/backend/src/models/Role.ts
+++ b/packages/backend/src/models/Role.ts
@@ -6,69 +6,149 @@
import { Entity, Column, PrimaryColumn } from 'typeorm';
import { id } from './util/id.js';
+/**
+ * ~かつ~
+ * 複数の条件を同時に満たす場合のみ成立とする
+ */
type CondFormulaValueAnd = {
type: 'and';
values: RoleCondFormulaValue[];
};
+/**
+ * ~または~
+ * 複数の条件のうち、いずれかを満たす場合のみ成立とする
+ */
type CondFormulaValueOr = {
type: 'or';
values: RoleCondFormulaValue[];
};
+/**
+ * ~ではない
+ * 条件を満たさない場合のみ成立とする
+ */
type CondFormulaValueNot = {
type: 'not';
value: RoleCondFormulaValue;
};
+/**
+ * ローカルユーザーのみ成立とする
+ */
type CondFormulaValueIsLocal = {
type: 'isLocal';
};
+/**
+ * リモートユーザーのみ成立とする
+ */
type CondFormulaValueIsRemote = {
type: 'isRemote';
};
+/**
+ * 既に指定のマニュアルロールにアサインされている場合のみ成立とする
+ */
type CondFormulaValueRoleAssignedTo = {
type: 'roleAssignedTo';
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: 'createdLessThan';
sec: number;
};
+/**
+ * ユーザが作成されてから指定期間経っていない場合のみ成立とする
+ */
type CondFormulaValueCreatedMoreThan = {
type: 'createdMoreThan';
sec: number;
};
+/**
+ * フォロワー数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueFollowersLessThanOrEq = {
type: 'followersLessThanOrEq';
value: number;
};
+/**
+ * フォロワー数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueFollowersMoreThanOrEq = {
type: 'followersMoreThanOrEq';
value: number;
};
+/**
+ * フォロー数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueFollowingLessThanOrEq = {
type: 'followingLessThanOrEq';
value: number;
};
+/**
+ * フォロー数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueFollowingMoreThanOrEq = {
type: 'followingMoreThanOrEq';
value: number;
};
+/**
+ * 投稿数が指定値以下の場合のみ成立とする
+ */
type CondFormulaValueNotesLessThanOrEq = {
type: 'notesLessThanOrEq';
value: number;
};
+/**
+ * 投稿数が指定値以上の場合のみ成立とする
+ */
type CondFormulaValueNotesMoreThanOrEq = {
type: 'notesMoreThanOrEq';
value: number;
@@ -80,6 +160,11 @@ export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueNot |
CondFormulaValueIsLocal |
CondFormulaValueIsRemote |
+ CondFormulaValueIsSuspended |
+ CondFormulaValueIsLocked |
+ CondFormulaValueIsBot |
+ CondFormulaValueIsCat |
+ CondFormulaValueIsExplorable |
CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan |
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index c7702505039d..d9987a70c381 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -57,6 +57,20 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
},
} 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 = {
type: 'object',
properties: {
@@ -135,6 +149,9 @@ export const packedRoleCondFormulaValueSchema = {
{
ref: 'RoleCondFormulaValueIsLocalOrRemote',
},
+ {
+ ref: 'RoleCondFormulaValueUserSettingBooleanSchema',
+ },
{
ref: 'RoleCondFormulaValueAssignedRole',
},
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 19d03570e00d..ec441735d7e6 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
@@ -20,6 +22,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { RoleCondFormulaValue } from '@/models/Role.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@@ -52,12 +55,26 @@ describe('RoleService', () => {
id: genAidx(Date.now()),
updatedAt: new Date(),
lastUsedAt: new Date(),
+ name: '',
description: '',
...data,
})
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
}
+ function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial = {}) {
+ return createRole({
+ name: `[conditional] ${condFormula.type}`,
+ target: 'conditional',
+ condFormula: condFormula,
+ ...data,
+ });
+ }
+
+ function aidx() {
+ return genAidx(Date.now());
+ }
+
beforeEach(async () => {
clock = lolex.install({
now: new Date(),
@@ -73,6 +90,7 @@ describe('RoleService', () => {
CacheService,
IdService,
GlobalEventService,
+ UserEntityService,
{
provide: NotificationService,
useFactory: () => ({
@@ -209,15 +227,9 @@ describe('RoleService', () => {
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({
+ test('expired role', async () => {
+ const user = await createUser();
+ const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
@@ -226,35 +238,133 @@ describe('RoleService', () => {
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,
- }],
- },
});
-
+ await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
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);
+ const result = await roleService.getUserPolicies(user.id);
+ expect(result.canManageCustomEmojis).toBe(true);
+
+ clock.tick('25:00:00');
+
+ const resultAfter25h = await roleService.getUserPolicies(user.id);
+ expect(resultAfter25h.canManageCustomEmojis).toBe(false);
+
+ await roleService.assign(user.id, role.id);
+
+ // ストリーミング経由で反映されるまでちょっと待つ
+ clock.uninstall();
+ await sleep(100);
+
+ const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
+ expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
+ });
+ });
+
+ 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 () => {
+ 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(),
@@ -262,15 +372,10 @@ describe('RoleService', () => {
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,
- },
+ const role2 = await createConditionalRole({
+ id: aidx(),
+ type: 'roleAssignedTo',
+ roleId: role1.id,
});
await roleService.assign(user2.id, role1.id);
@@ -282,41 +387,302 @@ describe('RoleService', () => {
expect(u2role.some(r => r.id === role2.id)).toBe(true);
});
- test('expired role', async () => {
- const user = await createUser();
- const role = await createRole({
- name: 'a',
- policies: {
- canManageCustomEmojis: {
- useDefault: false,
- priority: 0,
- value: true,
- },
- },
+ test('ローカルユーザのみ', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ host: null }),
+ createUser({ host: 'example.com' }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isLocal',
});
- await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
- metaService.fetch.mockResolvedValue({
- policies: {
- canManageCustomEmojis: false,
- },
- } as any);
- const result = await roleService.getUserPolicies(user.id);
- expect(result.canManageCustomEmojis).toBe(true);
+ 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);
+ });
- clock.tick('25:00:00');
+ test('リモートユーザのみ', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ host: null }),
+ createUser({ host: 'example.com' }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isRemote',
+ });
- const resultAfter25h = await roleService.getUserPolicies(user.id);
- expect(resultAfter25h.canManageCustomEmojis).toBe(false);
+ 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);
+ });
- await roleService.assign(user.id, role.id);
+ test('サスペンド済みユーザである', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ isSuspended: false }),
+ createUser({ isSuspended: true }),
+ ]);
+ const role = await createConditionalRole({
+ id: aidx(),
+ type: 'isSuspended',
+ });
- // ストリーミング経由で反映されるまでちょっと待つ
- clock.uninstall();
- await sleep(100);
+ 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);
+ });
- const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
- expect(resultAfter25hAgain.canManageCustomEmojis).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);
});
});
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 2f5b4c47d87e..f001a4ac2031 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -9,6 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+
+
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 360724d2a9e3..9720b04e3963 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1713,6 +1713,7 @@ declare namespace entities {
RoleCondFormulaLogics,
RoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote,
+ RoleCondFormulaValueUserSettingBooleanSchema,
RoleCondFormulaValueAssignedRole,
RoleCondFormulaValueCreated,
RoleCondFormulaFollowersOrFollowingOrNotes,
@@ -2745,6 +2746,9 @@ type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormul
// @public (undocumented)
type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
+// @public (undocumented)
+type RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'];
+
// @public (undocumented)
type RoleLite = components['schemas']['RoleLite'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 6f61458600f4..a6e5fbe6890e 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -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 RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'];
export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index ae001cf874c6..131d20f09bd2 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4586,6 +4586,11 @@ export type components = {
/** @enum {string} */
type: 'isLocal' | 'isRemote';
};
+ RoleCondFormulaValueUserSettingBooleanSchema: {
+ id: string;
+ /** @enum {string} */
+ type: 'isSuspended' | 'isLocked' | 'isBot' | 'isCat' | 'isExplorable';
+ };
RoleCondFormulaValueAssignedRole: {
id: string;
/** @enum {string} */
@@ -4608,7 +4613,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']['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: {
/**
* Format: id
From f9aed8f2bf994902386878d1212912caa3a57b0d Mon Sep 17 00:00:00 2001
From: anatawa12
Date: Fri, 19 Apr 2024 19:42:01 +0900
Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=AD=A3=E8=A6=8F=E5=8C=96=E3=81=95?=
=?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E7=8A=B6=E6=85=8B?=
=?UTF-8?q?=E3=81=AEhashtag=E3=81=8C=E9=80=A3=E5=90=88=E3=81=95=E3=82=8C?=
=?UTF-8?q?=E3=81=A6=E3=81=8D=E3=81=9Fhtml=E3=81=AB=E5=90=AB=E3=81=BE?=
=?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E3=81=A8hashtag=E3=81=8C?=
=?UTF-8?q?=E6=AD=A3=E3=81=97=E3=81=8Fhashtag=E3=81=AB=E5=BE=A9=E5=85=83?=
=?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?=
=?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#13733)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 1 +
packages/backend/src/core/MfmService.ts | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c13fa664dd53..8c8bcf0ea454 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
- 鍵アカウントユーザーか
- 「アカウントを見つけやすくする」が有効なユーザーか
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
+- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index c62ee5a642a4..2fb731201bd2 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -10,6 +10,7 @@ import { Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
+import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
@@ -33,6 +34,8 @@ export class MfmService {
// some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/
\r?\n/gi, '\n');
+ const normalizedHashtagNames = hashtagNames == null ? undefined : new Set(hashtagNames.map(x => normalizeForSearch(x)));
+
const dom = parse5.parseFragment(html);
let text = '';
@@ -85,7 +88,7 @@ export class MfmService {
const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ
- if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
+ if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt;
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {