From b60c985203b3657865f5047d228d1b63b373357d Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 28 Feb 2023 15:31:48 +0900 Subject: fix(server): メールアドレス更新時にバリデーションが正しく行われていないのを修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/endpoints/i/update-email.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index b656c5c51d..4f543a6472 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -73,8 +73,8 @@ export default class extends Endpoint { } if (ps.email != null) { - const available = await this.emailService.validateEmailForAccount(ps.email); - if (!available) { + const res = await this.emailService.validateEmailForAccount(ps.email); + if (!res.available) { throw new ApiError(meta.errors.unavailable); } } -- cgit v1.2.3-freya From 1c5291f8185651c231903129ee7c1cee263f9f03 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 1 Mar 2023 10:20:03 +0900 Subject: feat: 時限ロール (#10145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 時限ロール * クライアントから期限を確認できるように * リファクタとか * fix test * fix test * fix test * clean up --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 + .../1677570181236-role-assignment-expires-at.js | 13 + packages/backend/src/core/RoleService.ts | 77 ++++- .../backend/src/core/entities/RoleEntityService.ts | 13 +- .../backend/src/models/entities/RoleAssignment.ts | 6 + .../src/queue/processors/CleanProcessorService.ts | 18 +- .../src/server/api/endpoints/admin/roles/assign.ts | 29 +- .../server/api/endpoints/admin/roles/unassign.ts | 22 +- .../src/server/api/endpoints/admin/roles/users.ts | 6 + .../src/server/api/endpoints/roles/users.ts | 5 + packages/backend/test/unit/RoleService.ts | 75 +++-- packages/backend/test/utils.ts | 317 --------------------- packages/frontend/src/pages/admin/roles.role.vue | 47 ++- packages/frontend/src/pages/user-info.vue | 26 +- packages/frontend/src/scripts/get-user-menu.ts | 28 +- 16 files changed, 295 insertions(+), 390 deletions(-) create mode 100644 packages/backend/migration/1677570181236-role-assignment-expires-at.js (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be07e3244..ee941d479f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ You should also include the user name that made the change. ## 13.x.x (unreleased) ### Improvements +- 時限ロール - プッシュ通知でカスタム絵文字リアクションを表示できるように - アンテナでCWも検索対象にするように - ノートの操作部をホバー時のみ表示するオプションを追加 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ef5e1853b8..fa0cf67e5d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -848,11 +848,13 @@ instanceDefaultLightTheme: "インスタンスデフォルトのライトテー instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" mutePeriod: "ミュートする期限" +period: "期限" indefinitely: "無期限" tenMinutes: "10分" oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" +oneMonth: "1ヶ月" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" rateLimitExceeded: "レート制限を超えました" diff --git a/packages/backend/migration/1677570181236-role-assignment-expires-at.js b/packages/backend/migration/1677570181236-role-assignment-expires-at.js new file mode 100644 index 0000000000..3ac2edab0a --- /dev/null +++ b/packages/backend/migration/1677570181236-role-assignment-expires-at.js @@ -0,0 +1,13 @@ +export class roleAssignmentExpiresAt1677570181236 { + name = 'roleAssignmentExpiresAt1677570181236' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role_assignment" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`CREATE INDEX "IDX_539b6c08c05067599743bb6389" ON "role_assignment" ("expiresAt") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_539b6c08c05067599743bb6389"`); + await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "expiresAt"`); + } +} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index b84d5e7585..7149591198 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { StreamMessages } from '@/server/api/stream/types.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { @@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown { private rolesCache: Cache; private roleAssignmentByUserIdCache: Cache; + public static AlreadyAssignedError = class extends Error {}; + public static NotAssignedError = class extends Error {}; + constructor( @Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis, @@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown { private metaService: MetaService, private userCacheService: UserCacheService, private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private idService: IdService, ) { //this.onMessage = this.onMessage.bind(this); @@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown { cached.push({ ...body, createdAt: new Date(body.createdAt), + expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, }); } break; @@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown { @bindThis public async getUserRoles(userId: User['id']) { - const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const now = Date.now(); + 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(null, () => this.rolesRepository.findBy({})); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); @@ -207,7 +218,10 @@ export class RoleService implements OnApplicationShutdown { */ @bindThis public async getUserBadgeRoles(userId: User['id']) { - const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const now = Date.now(); + 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(null, () => this.rolesRepository.findBy({})); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); @@ -316,6 +330,65 @@ export class RoleService implements OnApplicationShutdown { return users; } + @bindThis + public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise { + const now = new Date(); + + const existing = await this.roleAssignmentsRepository.findOneBy({ + roleId: roleId, + userId: userId, + }); + + if (existing) { + if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) { + await this.roleAssignmentsRepository.delete({ + roleId: roleId, + userId: userId, + }); + } else { + throw new RoleService.AlreadyAssignedError(); + } + } + + const created = await this.roleAssignmentsRepository.insert({ + id: this.idService.genId(), + createdAt: now, + expiresAt: expiresAt, + roleId: roleId, + userId: userId, + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + + this.rolesRepository.update(roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleAssigned', created); + } + + @bindThis + public async unassign(userId: User['id'], roleId: Role['id']): Promise { + const now = new Date(); + + const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); + if (existing == null) { + throw new RoleService.NotAssignedError(); + } else if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) { + await this.roleAssignmentsRepository.delete({ + roleId: roleId, + userId: userId, + }); + throw new RoleService.NotAssignedError(); + } + + await this.roleAssignmentsRepository.delete(existing.id); + + this.rolesRepository.update(roleId, { + lastUsedAt: now, + }); + + this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); + } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onMessage); diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 80ef5ac1fa..4208e03a8c 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; @@ -28,9 +29,13 @@ export class RoleEntityService { ) { const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); - const assigns = await this.roleAssignmentsRepository.findBy({ - roleId: role.id, - }); + const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') + .where('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NOT NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) + .getCount(); const policies = { ...role.policies }; for (const [k, v] of Object.entries(DEFAULT_POLICIES)) { @@ -57,7 +62,7 @@ export class RoleEntityService { asBadge: role.asBadge, canEditMembersByModerator: role.canEditMembersByModerator, policies: policies, - usersCount: assigns.length, + usersCount: assignedCount, }); } diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts index e86f2a8999..972810940f 100644 --- a/packages/backend/src/models/entities/RoleAssignment.ts +++ b/packages/backend/src/models/entities/RoleAssignment.ts @@ -39,4 +39,10 @@ export class RoleAssignment { }) @JoinColumn() public role: Role | null; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date | null; } diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 406184cbde..7fd2cde9c0 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; -import { LessThan } from 'typeorm'; +import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -29,6 +29,9 @@ export class CleanProcessorService { @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + private queueLoggerService: QueueLoggerService, private idService: IdService, ) { @@ -56,6 +59,17 @@ export class CleanProcessorService { id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), }); + const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') + .where('assign.expiresAt IS NOT NULL') + .andWhere('assign.expiresAt < :now', { now: new Date() }) + .getMany(); + + if (expiredRoleAssignments.length > 0) { + await this.roleAssignmentsRepository.delete({ + id: In(expiredRoleAssignments.map(x => x.id)), + }); + } + this.logger.succ('Cleaned.'); done(); } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index 7bfb2f6625..b80aaba122 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js'; export const meta = { @@ -39,6 +37,10 @@ export const paramDef = { properties: { roleId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' }, + expiresAt: { + type: 'integer', + nullable: true, + }, }, required: [ 'roleId', @@ -56,12 +58,7 @@ export default class extends Endpoint { @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - @Inject(DI.roleAssignmentsRepository) - private roleAssignmentsRepository: RoleAssignmentsRepository, - - private globalEventService: GlobalEventService, private roleService: RoleService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); @@ -78,19 +75,11 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchUser); } - const date = new Date(); - const created = await this.roleAssignmentsRepository.insert({ - id: this.idService.genId(), - createdAt: date, - roleId: role.id, - userId: user.id, - }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + if (ps.expiresAt && ps.expiresAt <= Date.now()) { + return; + } - this.rolesRepository.update(ps.roleId, { - lastUsedAt: new Date(), - }); - - this.globalEventService.publishInternalEvent('userRoleAssigned', created); + await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index 141cc5ee89..45c4f76943 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js'; export const meta = { @@ -62,12 +60,7 @@ export default class extends Endpoint { @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - @Inject(DI.roleAssignmentsRepository) - private roleAssignmentsRepository: RoleAssignmentsRepository, - - private globalEventService: GlobalEventService, private roleService: RoleService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); @@ -84,18 +77,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchUser); } - const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id }); - if (roleAssignment == null) { - throw new ApiError(meta.errors.notAssigned); - } - - await this.roleAssignmentsRepository.delete(roleAssignment.id); - - this.rolesRepository.update(ps.roleId, { - lastUsedAt: new Date(), - }); - - this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment); + await this.roleService.unassign(user.id, role.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index bb016a8425..ec1e86fec9 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; @@ -56,6 +57,10 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NOT NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) .innerJoinAndSelect('assign.user', 'user'); const assigns = await query @@ -65,6 +70,7 @@ export default class extends Endpoint { return await Promise.all(assigns.map(async assign => ({ id: assign.id, user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + expiresAt: assign.expiresAt, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 6e221b6c67..2a8684e6d7 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; @@ -56,6 +57,10 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NOT NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) .innerJoinAndSelect('assign.user', 'user'); const assigns = await query diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 221f743d3a..6fe04274e6 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -3,16 +3,18 @@ process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { DataSource } from 'typeorm'; +import * as lolex from '@sinonjs/fake-timers'; import rndstr from 'rndstr'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { CoreModule } from '@/core/CoreModule.js'; import { MetaService } from '@/core/MetaService.js'; import { genAid } from '@/misc/id/aid.js'; import { UserCacheService } from '@/core/UserCacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -25,6 +27,7 @@ describe('RoleService', () => { let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; let metaService: jest.Mocked; + let clock: lolex.InstalledClock; function createUser(data: Partial = {}) { const un = rndstr('a-z0-9', 16); @@ -50,16 +53,12 @@ describe('RoleService', () => { .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); } - async function assign(roleId: Role['id'], userId: User['id']) { - await roleAssignmentsRepository.insert({ - id: genAid(new Date()), - createdAt: new Date(), - roleId, - userId, + beforeEach(async () => { + clock = lolex.install({ + now: new Date(), + shouldClearNativeTimers: true, }); - } - beforeEach(async () => { app = await Test.createTestingModule({ imports: [ GlobalModule, @@ -67,6 +66,8 @@ describe('RoleService', () => { providers: [ RoleService, UserCacheService, + IdService, + GlobalEventService, ], }) .useMocker((token) => { @@ -92,12 +93,15 @@ describe('RoleService', () => { }); afterEach(async () => { + clock.uninstall(); + await Promise.all([ app.get(DI.metasRepository).delete({}), usersRepository.delete({}), rolesRepository.delete({}), roleAssignmentsRepository.delete({}), ]); + await app.close(); }); @@ -115,7 +119,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(false); }); - test('instance default policies 2', async () => { + test('instance default policies 2', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ policies: { @@ -128,7 +132,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(true); }); - test('with role', async () => { + test('with role', async () => { const user = await createUser(); const role = await createRole({ name: 'a', @@ -140,7 +144,7 @@ describe('RoleService', () => { }, }, }); - await assign(role.id, user.id); + await roleService.assign(user.id, role.id); metaService.fetch.mockResolvedValue({ policies: { canManageCustomEmojis: false, @@ -152,7 +156,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(true); }); - test('priority', async () => { + test('priority', async () => { const user = await createUser(); const role1 = await createRole({ name: 'role1', @@ -174,8 +178,8 @@ describe('RoleService', () => { }, }, }); - await assign(role1.id, user.id); - await assign(role2.id, user.id); + await roleService.assign(user.id, role1.id); + await roleService.assign(user.id, role2.id); metaService.fetch.mockResolvedValue({ policies: { driveCapacityMb: 50, @@ -187,7 +191,7 @@ describe('RoleService', () => { expect(result.driveCapacityMb).toBe(100); }); - test('conditional role', async () => { + test('conditional role', async () => { const user1 = await createUser({ createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)), }); @@ -228,5 +232,42 @@ describe('RoleService', () => { expect(user1Policies.canManageCustomEmojis).toBe(false); expect(user2Policies.canManageCustomEmojis).toBe(true); }); + + test('expired role', async () => { + const user = await createUser(); + const role = await createRole({ + name: 'a', + policies: { + canManageCustomEmojis: { + useDefault: false, + priority: 0, + value: true, + }, + }, + }); + 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); + + 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); + }); }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 50988939aa..b813362893 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,320 +1,3 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as childProcess from 'child_process'; -import * as http from 'node:http'; -import { SIGKILL } from 'constants'; -import WebSocket from 'ws'; -import fetch from 'node-fetch'; -import FormData from 'form-data'; -import { DataSource } from 'typeorm'; -import got, { RequestError } from 'got'; -import loadConfig from '../src/config/load.js'; -import { entities } from '@/postgres.js'; -import type * as misskey from 'misskey-js'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const config = loadConfig(); -export const port = config.port; - -export const api = async (endpoint: string, params: any, me?: any) => { - endpoint = endpoint.replace(/^\//, ''); - - const auth = me ? { - i: me.token, - } : {}; - - try { - const res = await got(`http://localhost:${port}/api/${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), - retry: { - limit: 0, - }, - }); - - const status = res.statusCode; - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; - - return { - status, - body, - }; - } catch (err: unknown) { - if (err instanceof RequestError && err.response) { - const status = err.response.statusCode; - const body = await JSON.parse(err.response.body as string); - - return { - status, - body, - }; - } else { - throw err; - } - } -}; - -export const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { - const auth = me ? { - i: me.token, - } : {}; - - const res = await fetch(`http://localhost:${port}/${path}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), - }); - - const status = res.status; - const body = res.status === 200 ? await res.json().catch() : null; - - return { - body, status, - }; -}; - -export const signup = async (params?: any): Promise => { - const q = Object.assign({ - username: 'test', - password: 'test', - }, params); - - const res = await api('signup', q); - - return res.body; -}; - -export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise => { - const q = Object.assign({ - text: 'test', - }, params); - - const res = await api('notes/create', q, user); - - return res.body ? res.body.createdNote : null; -}; - -export const react = async (user: any, note: any, reaction: string): Promise => { - await api('notes/reactions/create', { - noteId: note.id, - reaction: reaction, - }, user); -}; - -/** - * Upload file - * @param user User - * @param _path Optional, absolute path or relative from ./resources/ - */ -export const uploadFile = async (user: any, _path?: string): Promise => { - const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`; - - const formData = new FormData() as any; - formData.append('i', user.token); - formData.append('file', fs.createReadStream(absPath)); - formData.append('force', 'true'); - - const res = await got(`http://localhost:${port}/api/drive/files/create`, { - method: 'POST', - body: formData, - retry: { - limit: 0, - }, - }); - - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; - - return body; -}; - -export const uploadUrl = async (user: any, url: string) => { - let file: any; - const marker = Math.random().toString(); - - const ws = await connectStream(user, 'main', (msg) => { - if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { - file = msg.body.file; - } - }); - - await api('drive/files/upload-from-url', { - url, - marker, - force: true, - }, user); - - await sleep(7000); - ws.close(); - - return file; -}; - -export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { - return new Promise((res, rej) => { - const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`); - - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type === 'channel' && msg.body.id === 'a') { - listener(msg.body); - } else if (msg.type === 'connected' && msg.body.id === 'a') { - res(ws); - } - }); - - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: channel, - id: 'a', - pong: true, - params: params, - }, - })); - }); - }); -} - -export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { - return new Promise(async (res, rej) => { - let timer: NodeJS.Timeout; - - let ws: WebSocket; - try { - ws = await connectStream(user, channel, msg => { - if (cond(msg)) { - ws.close(); - if (timer) clearTimeout(timer); - res(true); - } - }, params); - } catch (e) { - rej(e); - } - - if (!ws!) return; - - timer = setTimeout(() => { - ws.close(); - res(false); - }, 3000); - - try { - await trgr(); - } catch (e) { - ws.close(); - if (timer) clearTimeout(timer); - rej(e); - } - }); -}; - -export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => { - // node-fetchだと3xxを取れない - return await new Promise((resolve, reject) => { - const req = http.request(`http://localhost:${port}${path}`, { - headers: { - Accept: accept, - }, - }, res => { - if (res.statusCode! >= 400) { - reject(res); - } else { - resolve({ - status: res.statusCode, - type: res.headers['content-type'], - location: res.headers.location, - }); - } - }); - - req.end(); - }); -}; - -export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise = async () => {}) { - return (done: (err?: Error) => any) => { - const p = childProcess.spawn('node', [_dirname + '/../index.js'], { - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - env: { NODE_ENV: 'test', PATH: process.env.PATH }, - }); - callbackSpawnedProcess(p); - p.on('message', message => { - if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e)); - }); - }; -} - -export async function initTestDb(justBorrow = false, initEntities?: any[]) { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; - - const db = new DataSource({ - type: 'postgres', - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, - entities: initEntities ?? entities, - }); - - await db.initialize(); - - return db; -} - -export function startServer(timeout = 60 * 1000): Promise { - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - rej('timeout to start'); - }, timeout); - - const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], { - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - env: { NODE_ENV: 'test', PATH: process.env.PATH }, - }); - - p.on('error', e => rej(e)); - - p.on('message', message => { - if (message === 'ok') { - clearTimeout(t); - res(p); - } - }); - }); -} - -export function shutdownServer(p: childProcess.ChildProcess | undefined, timeout = 20 * 1000) { - if (p == null) return Promise.resolve('nop'); - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - res('force exit'); - }, timeout); - - p.once('exit', () => { - clearTimeout(t); - res('exited'); - }); - - p.kill(); - }); -} - export function sleep(msec: number) { return new Promise(res => { setTimeout(() => { diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index e7d57ad4f0..7951086bf1 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -34,6 +34,7 @@ + @@ -98,13 +99,37 @@ async function del() { router.push('/admin/roles'); } -function assign() { - os.selectUser({ +async function assign() { + const user = await os.selectUser({ includeSelf: true, - }).then(async (user) => { - await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id }); - role.users.push(user); }); + + const { canceled: canceled2, result: period } = await os.select({ + title: i18n.ts.period, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }, { + value: 'oneMonth', text: i18n.ts.oneMonth, + }], + default: 'indefinitely', + }); + if (canceled2) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) + : null; + + await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt }); + role.users.push(user); } async function unassign(user, ev) { @@ -119,6 +144,13 @@ async function unassign(user, ev) { }], ev.currentTarget ?? ev.target); } +async function showExpireInfo(assignment) { + os.alert({ + type: 'info', + text: assignment.expiresAt.toLocaleString(), + }); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); @@ -139,10 +171,15 @@ definePageMetadata(computed(() => ({ min-width: 0; } +.expiresAt, .unassign { width: 32px; height: 32px; margin-left: 8px; align-self: center; } + +.expiresAt + .unassign { + margin-left: 0; +} diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index 13a06286f6..373af193d7 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -337,7 +337,31 @@ async function assignRole() { }); if (canceled) return; - await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id }); + const { canceled: canceled2, result: period } = await os.select({ + title: i18n.ts.period, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }, { + value: 'oneMonth', text: i18n.ts.oneMonth, + }], + default: 'indefinitely', + }); + if (canceled2) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) + : null; + + await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt }); refreshUser(); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 6c6baf8266..5170ca4c8c 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -143,8 +143,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router return roles.filter(r => r.target === 'manual').map(r => ({ text: r.name, - action: () => { - os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id }); + action: async () => { + const { canceled, result: period } = await os.select({ + title: i18n.ts.period, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }, { + value: 'oneMonth', text: i18n.ts.oneMonth, + }], + default: 'indefinitely', + }); + if (canceled) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) + : null; + + os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt }); }, })); }, -- cgit v1.2.3-freya From 9e5278d2760a66c3d8fadadbe45ccf2f09da1130 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 1 Mar 2023 12:02:37 +0900 Subject: fix of #10145 --- packages/backend/src/core/entities/RoleEntityService.ts | 2 +- packages/backend/src/server/api/endpoints/admin/roles/users.ts | 2 +- packages/backend/src/server/api/endpoints/roles/users.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 4208e03a8c..2f1d51fa1a 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -32,7 +32,7 @@ export class RoleEntityService { const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') .where('assign.roleId = :roleId', { roleId: role.id }) .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NOT NULL') + .where('assign.expiresAt IS NULL') .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .getCount(); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index ec1e86fec9..930d3fee40 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -58,7 +58,7 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NOT NULL') + .where('assign.expiresAt IS NULL') .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 2a8684e6d7..607dc24206 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -58,7 +58,7 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NOT NULL') + .where('assign.expiresAt IS NULL') .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); -- cgit v1.2.3-freya From d0bbeeee526543b48e3143231fabaa0c0e10e0c4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 1 Mar 2023 14:22:53 +0900 Subject: :art: --- .../src/server/api/endpoints/admin/roles/users.ts | 1 + packages/frontend/src/pages/admin/roles.role.vue | 65 ++++++++++----- packages/frontend/src/pages/my-lists/list.vue | 96 ++++++++++------------ 3 files changed, 90 insertions(+), 72 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 930d3fee40..35edca5460 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -69,6 +69,7 @@ export default class extends Endpoint { return await Promise.all(assigns.map(async assign => ({ id: assign.id, + createdAt: assign.createdAt, user: await this.userEntityService.pack(assign.user!, me, { detail: true }), expiresAt: assign.expiresAt, }))); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 7951086bf1..c5792f649c 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -30,12 +30,19 @@ @@ -77,6 +84,8 @@ const usersPagination = { })), }; +let expandedItems = $ref([]); + const role = reactive(await os.api('admin/roles/show', { roleId: props.id, })); @@ -129,7 +138,7 @@ async function assign() { : null; await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt }); - role.users.push(user); + //role.users.push(user); } async function unassign(user, ev) { @@ -139,16 +148,17 @@ async function unassign(user, ev) { danger: true, action: async () => { await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id }); - role.users = role.users.filter(u => u.id !== user.id); + //role.users = role.users.filter(u => u.id !== user.id); }, }], ev.currentTarget ?? ev.target); } -async function showExpireInfo(assignment) { - os.alert({ - type: 'info', - text: assignment.expiresAt.toLocaleString(), - }); +async function toggleItem(item) { + if (expandedItems.includes(item.id)) { + expandedItems = expandedItems.filter(x => x !== item.id); + } else { + expandedItems.push(item.id); + } } const headerActions = $computed(() => []); @@ -162,24 +172,41 @@ definePageMetadata(computed(() => ({ diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index f47b4bf90f..a6a3974d0c 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -2,13 +2,13 @@ -
+
-
+
{{ i18n.ts.addUser }} {{ i18n.ts.rename }} - {{ i18n.ts.delete }} + {{ i18n.ts.delete }}
@@ -16,18 +16,12 @@
{{ i18n.ts.members }}
-
-
-
- -
- - -
-
- -
-
+
+
+ + + +
@@ -44,6 +38,8 @@ import * as os from '@/os'; import { mainRouter } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; +import { userPage } from '@/filters/user'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; const props = defineProps<{ listId: string; @@ -76,13 +72,20 @@ function addUser() { }); } -function removeUser(user) { - os.api('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - users = users.filter(x => x.id !== user.id); - }); +async function removeUser(user, ev) { + os.popupMenu([{ + text: i18n.ts.remove, + icon: 'ti ti-x', + danger: true, + action: async () => { + os.api('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + users = users.filter(x => x.id !== user.id); + }); + }, + }], ev.currentTarget ?? ev.target); } async function renameList() { @@ -126,37 +129,24 @@ definePageMetadata(computed(() => list ? { } : null)); - -- cgit v1.2.3-freya From 53987fadd77231e532f9e3501152c1c8be9c25d7 Mon Sep 17 00:00:00 2001 From: kabo2468 <28654659+kabo2468@users.noreply.github.com> Date: Fri, 3 Mar 2023 10:30:21 +0900 Subject: fix(server): チャンネルでミュートが正しく機能していないのを修正 (#10166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(server): チャンネルでミュートが正しく機能していないのを修正 * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/channels/timeline.ts | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb17a0406..199660542f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ You should also include the user name that made the change. ### Bugfixes - 外部メディアプロキシ使用時にアバタークロップができない問題を修正 - fix(server): メールアドレス更新時にバリデーションが正しく行われていないのを修正 +- fix(server): チャンネルでミュートが正しく機能していないのを修正 ## 13.8.1 (2023/02/26) diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 58f8835279..cdaa400137 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -82,6 +82,12 @@ export default class extends Endpoint { .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } //#endregion const timeline = await query.take(ps.limit).getMany(); -- cgit v1.2.3-freya