diff options
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
11 files changed, 367 insertions, 37 deletions
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index b2d9cea03c..c59479d370 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, AntennasRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; @@ -14,6 +15,7 @@ import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { trackPromise } from '@/misc/promise-tracker.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -69,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private queryService: QueryService, private fanoutTimelineService: FanoutTimelineService, private globalEventService: GlobalEventService, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -108,6 +111,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + // -- ミュートされたチャンネル対策 + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + if (mutingChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.channelId IS NULL'); + qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL'); + qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + } + // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。 // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255 diff --git a/packages/backend/src/server/api/endpoints/channels/mute/create.ts b/packages/backend/src/server/api/endpoints/channels/mute/create.ts new file mode 100644 index 0000000000..26ce707c7a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/mute/create.ts @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ChannelsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; + +export const meta = { + tags: ['channels', 'mute'], + + requireCredential: true, + prohibitMoved: true, + + kind: 'write:channels', + + errors: { + noSuchChannel: { + message: 'No such Channel.', + code: 'NO_SUCH_CHANNEL', + id: '7174361e-d58f-31d6-2e7c-6fb830786a3f', + }, + + alreadyMuting: { + message: 'You are already muting that user.', + code: 'ALREADY_MUTING_CHANNEL', + id: '5a251978-769a-da44-3e89-3931e43bb592', + }, + + expiresAtIsPast: { + message: 'Cannot set past date to "expiresAt".', + code: 'EXPIRES_AT_IS_PAST', + id: '42b32236-df2c-a45f-fdbf-def67268f749', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + channelId: { type: 'string', format: 'misskey:id' }, + expiresAt: { + type: 'integer', + nullable: true, + description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.', + }, + }, + required: ['channelId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + private channelMutingService: ChannelMutingService, + ) { + super(meta, paramDef, async (ps, me) => { + // Check if exists the channel + const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId }); + if (!targetChannel) { + throw new ApiError(meta.errors.noSuchChannel); + } + + // Check if already muting + const exist = await this.channelMutingService.isMuted({ + requestUserId: me.id, + targetChannelId: targetChannel.id, + }); + if (exist) { + throw new ApiError(meta.errors.alreadyMuting); + } + + // Check if expiresAt is past + if (ps.expiresAt && ps.expiresAt <= Date.now()) { + throw new ApiError(meta.errors.expiresAtIsPast); + } + + await this.channelMutingService.mute({ + requestUserId: me.id, + targetChannelId: targetChannel.id, + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/mute/delete.ts b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts new file mode 100644 index 0000000000..79abeebe99 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ChannelsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['channels', 'mute'], + + requireCredential: true, + prohibitMoved: true, + + kind: 'write:channels', + + errors: { + noSuchChannel: { + message: 'No such Channel.', + code: 'NO_SUCH_CHANNEL', + id: 'e7998769-6e94-d9c2-6b8f-94a527314aba', + }, + + notMuting: { + message: 'You are not muting that channel.', + code: 'NOT_MUTING_CHANNEL', + id: '14d55962-6ea8-d990-1333-d6bef78dc2ab', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + channelId: { type: 'string', format: 'misskey:id' }, + }, + required: ['channelId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + private channelMutingService: ChannelMutingService, + ) { + super(meta, paramDef, async (ps, me) => { + // Check if exists the channel + const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId }); + if (!targetChannel) { + throw new ApiError(meta.errors.noSuchChannel); + } + + // Check muting + const exist = await this.channelMutingService.isMuted({ + requestUserId: me.id, + targetChannelId: targetChannel.id, + }); + if (!exist) { + throw new ApiError(meta.errors.notMuting); + } + + await this.channelMutingService.unmute({ + requestUserId: me.id, + targetChannelId: targetChannel.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/mute/list.ts b/packages/backend/src/server/api/endpoints/channels/mute/list.ts new file mode 100644 index 0000000000..74338eea38 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/mute/list.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; + +export const meta = { + tags: ['channels', 'mute'], + + requireCredential: true, + prohibitMoved: true, + + kind: 'read:channels', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Channel', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private channelMutingService: ChannelMutingService, + private channelEntityService: ChannelEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const mutings = await this.channelMutingService.list({ + requestUserId: me.id, + }); + return await this.channelEntityService.packMany(mutings, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 46b050d4b4..4f56bc2110 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -70,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private queryService: QueryService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private activeUsersChart: ActiveUsersChart, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -98,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- useDbFallback: true, redisTimelines: [`channelTimeline:${channel.id}`], excludePureRenotes: false, + ignoreAuthorChannelFromMute: true, dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); }, @@ -122,6 +125,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('note.channel', 'channel'); this.queryService.generateBaseNoteFilteringQuery(query, me); + + if (me) { + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id).filter(x => x !== ps.channelId)); + if (mutingChannelIds.length > 0) { + query.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + } + } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 2c8459525a..0a3602df20 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -18,6 +18,8 @@ import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; +import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -46,7 +48,7 @@ export const meta = { bothWithRepliesAndWithFiles: { message: 'Specifying both withReplies and withFiles is not supported', code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', - id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f' + id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', }, }, } as const; @@ -79,9 +81,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - private noteEntityService: NoteEntityService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, @@ -89,6 +88,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private cacheService: CacheService, private queryService: QueryService, private userFollowingService: UserFollowingService, + private channelMutingService: ChannelMutingService, + private channelFollowingService: ChannelFollowingService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { @@ -196,11 +197,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withReplies: boolean, }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); - const followingChannels = await this.channelFollowingsRepository.find({ - where: { - followerId: me.id, - }, - }); + + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + const followingChannelIds = await this.channelFollowingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id).filter(x => !mutingChannelIds.includes(x))); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { @@ -219,9 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - if (followingChannels.length > 0) { - const followingChannelIds = followingChannels.map(x => x.followeeId); - + if (followingChannelIds.length > 0) { query.andWhere(new Brackets(qb => { qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); qb.orWhere('note.channelId IS NULL'); @@ -230,6 +231,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('note.channelId IS NULL'); } + if (mutingChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL'); + qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + } + if (!ps.withReplies) { query.andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index ee61ab43da..ec9e52cf04 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -15,6 +15,7 @@ import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -76,6 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -157,7 +159,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBaseNoteFilteringQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + if (me) { + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + + const mutedChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + if (mutedChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL') + .orWhere('note.renoteChannelId NOT IN (:...mutedChannelIds)', { mutedChannelIds }); + })); + } + } if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index eeeb797efc..fe9c412be4 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; +import type { NotesRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; +import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; export const meta = { tags: ['notes'], @@ -61,15 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - private noteEntityService: NoteEntityService, private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, + private channelMutingService: ChannelMutingService, + private channelFollowingService: ChannelFollowingService, private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { @@ -140,11 +141,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); - const followingChannels = await this.channelFollowingsRepository.find({ - where: { - followerId: me.id, - }, - }); + + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + const followingChannelIds = await this.channelFollowingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id).filter(x => !mutingChannelIds.includes(x))); //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -154,15 +157,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - if (followees.length > 0 && followingChannels.length > 0) { + if (followees.length > 0 && followingChannelIds.length > 0) { // ユーザー・チャンネルともにフォローあり const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - const followingChannelIds = followingChannels.map(x => x.followeeId); query.andWhere(new Brackets(qb => { qb .where(new Brackets(qb2 => { qb2 - .where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }) + .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }) .andWhere('note.channelId IS NULL'); })) .orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); @@ -170,22 +172,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } else if (followees.length > 0) { // ユーザーフォローのみ(チャンネルフォローなし) const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - } else if (followingChannels.length > 0) { + query.andWhere(new Brackets(qb => { + qb + .andWhere('note.channelId IS NULL') + .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + if (mutingChannelIds.length > 0) { + qb.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + } + })); + } else if (followingChannelIds.length > 0) { // チャンネルフォローのみ(ユーザーフォローなし) - const followingChannelIds = followingChannels.map(x => x.followeeId); query.andWhere(new Brackets(qb => { qb + // renoteChannelIdは見る必要が無い + // ・HTLに流れてくるチャンネル=フォローしているチャンネル + // ・HTLにフォロー外のチャンネルが流れるのは、フォローしているユーザがそのチャンネル投稿をリノートした場合のみ + // つまり、ユーザフォローしてない前提のこのブロックでは見る必要が無い .where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }) .orWhere('note.userId = :meId', { meId: me.id }); })); } else { // フォローなし - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId = :meId', { meId: me.id }); + query.andWhere(new Brackets(qb => { + qb + .andWhere('note.channelId IS NULL') + .andWhere('note.userId = :meId', { meId: me.id }); + })); } query.andWhere(new Brackets(qb => { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 42e80c6ae1..c0c8653f7b 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -84,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -187,6 +189,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateBaseNoteFilteringQuery(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + // -- ミュートされたチャンネルのリノート対策 + const mutedChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + if (mutedChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL') + .orWhere('note.renoteChannelId NOT IN (:...mutedChannelIds)', { mutedChannelIds }); + })); + } + if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { qb.orWhere('note.userId != :meId', { meId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index e8a760e9f8..4515c016a8 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, RolesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; @@ -12,6 +13,7 @@ import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -68,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private fanoutTimelineService: FanoutTimelineService, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -101,6 +104,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + // -- ミュートされたチャンネル対策 + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + if (mutingChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.channelId IS NULL'); + qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL'); + qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + } + this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBaseNoteFilteringQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 5832790a61..b9710250cf 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -16,6 +16,7 @@ import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { ApiError } from '@/server/api/error.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; export const meta = { tags: ['users', 'notes'], @@ -77,12 +78,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, - private noteEntityService: NoteEntityService, private queryService: QueryService, private cacheService: CacheService, private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private channelMutingService: ChannelMutingService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -165,6 +166,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withFiles: boolean, withRenotes: boolean, }, me: MiLocalUser | null) { + const mutingChannelIds = me + ? await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)) + : []; const isSelf = me && (me.id === ps.userId); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -177,14 +183,30 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); if (ps.withChannelNotes) { - if (!isSelf) query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); + query.andWhere(new Brackets(qb => { + if (mutingChannelIds.length > 0) { + qb.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds: mutingChannelIds }); + } + + if (!isSelf) { + qb.andWhere(new Brackets(qb2 => { + qb2.orWhere('note.channelId IS NULL'); + qb2.orWhere('channel.isSensitive = false'); + })); + } })); } else { query.andWhere('note.channelId IS NULL'); } + // -- ミュートされたチャンネルのリノート対策 + if (mutingChannelIds.length > 0) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL'); + qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); + } + this.queryService.generateVisibilityQuery(query, me); this.queryService.generateBaseNoteFilteringQuery(query, me, { excludeAuthor: true, |