diff --git a/locales/index.d.ts b/locales/index.d.ts index db7e3e957560..31b2c2a4aa28 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2012,6 +2012,7 @@ export interface Locale { "_timelines": { "home": string; "local": string; + "media": string; "social": string; "global": string; }; diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index ebe430d1058c..da86b2c1d3fc 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -30,7 +30,6 @@ import { HashtagChannelService } from './api/stream/channels/hashtag.js'; import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js'; import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js'; import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js'; -import { MediaTimelineChannelService } from './api/stream/channels/media-timeline.js'; import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; @@ -75,7 +74,6 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline. HomeTimelineChannelService, HybridTimelineChannelService, LocalTimelineChannelService, - MediaTimelineChannelService, QueueStatsChannelService, ServerStatsChannelService, UserListChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 46641c986fa5..4e6bc46e6778 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -258,7 +258,6 @@ import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; -import * as ep___notes_mediaTimeline from './endpoints/notes/media-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; @@ -604,7 +603,6 @@ const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep__ const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; -const $notes_mediaTimeline: Provider = { provide: 'ep:notes/media-timeline', useClass: ep___notes_mediaTimeline.default }; const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; @@ -954,7 +952,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_globalTimeline, $notes_hybridTimeline, $notes_localTimeline, - $notes_mediaTimeline, $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, @@ -1298,7 +1295,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_globalTimeline, $notes_hybridTimeline, $notes_localTimeline, - $notes_mediaTimeline, $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index d67ceb923b55..41c3a29eec02 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -258,7 +258,6 @@ import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; -import * as ep___notes_mediaTimeline from './endpoints/notes/media-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; @@ -602,7 +601,6 @@ const eps = [ ['notes/global-timeline', ep___notes_globalTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], - ['notes/media-timeline', ep___notes_mediaTimeline], ['notes/mentions', ep___notes_mentions], ['notes/polls/recommendation', ep___notes_polls_recommendation], ['notes/polls/vote', ep___notes_polls_vote], diff --git a/packages/backend/src/server/api/endpoints/notes/media-timeline.ts b/packages/backend/src/server/api/endpoints/notes/media-timeline.ts deleted file mode 100644 index ea36aeddcdd2..000000000000 --- a/packages/backend/src/server/api/endpoints/notes/media-timeline.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Brackets } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; -import ActiveUsersChart from '@/core/chart/charts/active-users.js'; -import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; -import { IdService } from '@/core/IdService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['notes'], - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - errors: { - ltlDisabled: { - message: 'Media timeline has been disabled.', - code: 'MTL_DISABLED', - id: '45a6eb02-7695-4393-b023-dd4be9aaaefd', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, - fileType: { type: 'array', items: { - type: 'string', - } }, - excludeNsfw: { type: 'boolean', default: false }, - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - sinceDate: { type: 'integer' }, - untilDate: { type: 'integer' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private noteEntityService: NoteEntityService, - private queryService: QueryService, - private metaService: MetaService, - private roleService: RoleService, - private activeUsersChart: ActiveUsersChart, - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const policies = await this.roleService.getUserPolicies(me ? me.id : null); - if (!policies.ltlAvailable) { - throw new ApiError(meta.errors.ltlDisabled); - } - - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') - .andWhere('note.fileIds != \'{}\'') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); - this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateMutedNoteQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); - } - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); - } - }); - - return await this.noteEntityService.packMany(timeline, me); - }); - } -} diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index a6e90cf2804f..4a544fadfe9b 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; import { LocalTimelineChannelService } from './channels/local-timeline.js'; -import { MediaTimelineChannelService } from './channels/media-timeline.js'; import { HomeTimelineChannelService } from './channels/home-timeline.js'; import { GlobalTimelineChannelService } from './channels/global-timeline.js'; import { MainChannelService } from './channels/main.js'; @@ -22,7 +21,6 @@ export class ChannelsService { private mainChannelService: MainChannelService, private homeTimelineChannelService: HomeTimelineChannelService, private localTimelineChannelService: LocalTimelineChannelService, - private mediaTimelineChannelService: MediaTimelineChannelService, private hybridTimelineChannelService: HybridTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, private userListChannelService: UserListChannelService, @@ -43,7 +41,6 @@ export class ChannelsService { case 'main': return this.mainChannelService; case 'homeTimeline': return this.homeTimelineChannelService; case 'localTimeline': return this.localTimelineChannelService; - case 'mediaTimeline': return this.mediaTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; case 'userList': return this.userListChannelService; diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index d3339072c1ef..77d4f0592a63 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -14,6 +14,7 @@ class GlobalTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = false; private withReplies: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -33,6 +34,7 @@ class GlobalTimelineChannel extends Channel { if (!policies.gtlAvailable) return; this.withReplies = params.withReplies as boolean; + this.withFiles = params.withFiles as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -43,6 +45,9 @@ class GlobalTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null) return; + // ファイルを含まない投稿は除外 + if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; + // リプライなら再pack if (note.replyId != null) { note.reply = await this.noteEntityService.pack(note.replyId, this.user, { diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index fe0cc37b6b9e..d740b2efb731 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -12,6 +12,7 @@ class HomeTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = true; private withReplies: boolean; + private withFiles: boolean; constructor( private noteEntityService: NoteEntityService, @@ -26,6 +27,7 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { this.withReplies = params.withReplies as boolean; + this.withFiles = params.withFiles as boolean; this.subscriber.on('notesStream', this.onNote); } @@ -42,6 +44,9 @@ class HomeTimelineChannel extends Channel { // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; + // ファイルを含まない投稿は除外 + if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; + if (['followers', 'specified'].includes(note.visibility)) { note = await this.noteEntityService.pack(note.id, this.user!, { detail: true, diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 5a33e13cf55b..6627621dc822 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -14,6 +14,7 @@ class HybridTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = true; private withReplies: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -33,6 +34,7 @@ class HybridTimelineChannel extends Channel { if (!policies.ltlAvailable) return; this.withReplies = params.withReplies as boolean; + this.withFiles = params.withFiles as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -51,6 +53,9 @@ class HybridTimelineChannel extends Channel { (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; + // ファイルを含まない投稿は除外 + if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; + if (['followers', 'specified'].includes(note.visibility)) { note = await this.noteEntityService.pack(note.id, this.user!, { detail: true, diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 9ca4db8ced92..30be8778ede4 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -13,6 +13,7 @@ class LocalTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = false; private withReplies: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -32,6 +33,7 @@ class LocalTimelineChannel extends Channel { if (!policies.ltlAvailable) return; this.withReplies = params.withReplies as boolean; + this.withFiles = params.withFiles as boolean; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -43,6 +45,9 @@ class LocalTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; + // ファイルを含まない投稿は除外 + if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; + // リプライなら再pack if (note.replyId != null) { note.reply = await this.noteEntityService.pack(note.replyId, this.user, { diff --git a/packages/backend/src/server/api/stream/channels/media-timeline.ts b/packages/backend/src/server/api/stream/channels/media-timeline.ts deleted file mode 100644 index 0855f169c61c..000000000000 --- a/packages/backend/src/server/api/stream/channels/media-timeline.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { MetaService } from '@/core/MetaService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; -import Channel from '../channel.js'; - -class MediaTimelineChannel extends Channel { - public readonly chName = 'mediaTimeline'; - public static shouldShare = true; - public static requireCredential = false; - - constructor( - private metaService: MetaService, - private roleService: RoleService, - private noteEntityService: NoteEntityService, - - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - //this.onNote = this.onNote.bind(this); - } - - @bindThis - public async init(params: any) { - const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); - if (!policies.ltlAvailable) return; - - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - @bindThis - private async onNote(note: Packed<'Note'>) { - if (note.user.host !== null) return; - if (note.visibility !== 'public') return; - if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await this.noteEntityService.pack(note.replyId, this.user, { - detail: true, - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { - detail: true, - }); - } - - // 関係ない返信は除外 - if (note.reply && this.user && !this.user.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.userIdsWhoMeMuting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - - if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - @bindThis - public dispose() { - // Unsubscribe events - this.subscriber.off('notesStream', this.onNote); - } -} - -@Injectable() -export class MediaTimelineChannelService { - public readonly shouldShare = MediaTimelineChannel.shouldShare; - public readonly requireCredential = MediaTimelineChannel.requireCredential; - - constructor( - private metaService: MetaService, - private roleService: RoleService, - private noteEntityService: NoteEntityService, - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): MediaTimelineChannel { - return new MediaTimelineChannel( - this.metaService, - this.roleService, - this.noteEntityService, - id, - connection, - ); - } -} diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 6fdba5fa210c..6164b03adc0d 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -38,18 +38,6 @@ const prepend = note => { } }; -const prependFilterdMedia = note => { - if (note.files !== null && note.files.length > 0) { - tlComponent.pagingComponent?.prepend(note); - } - - emit('note'); - - if (props.sound) { - sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note'); - } -}; - const onUserAdded = () => { tlComponent.pagingComponent?.reload(); }; @@ -95,9 +83,16 @@ if (props.src === 'antenna') { }); connection.on('note', prepend); } else if (props.src === 'media') { - endpoint = 'notes/media-timeline'; - connection = stream.useChannel('mediaTimeline'); - connection.on('note', prependFilterdMedia); + endpoint = 'notes/hybrid-timeline'; + query = { + withFiles: true, + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('hybridTimeline', { + withFiles: true, + withReplies: defaultStore.state.showTimelineReplies, + }); + connection.on('note', prepend); } else if (props.src === 'social') { endpoint = 'notes/hybrid-timeline'; query = { diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index b7d928229fa7..96ac7787e1e1 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -56,13 +56,6 @@ export type Channels = { }; receives: null; }; - mediaTimeline: { - params: null; - events: { - note: (payload: Note) => void; - }; - receives: null; - }; hybridTimeline: { params: null; events: {