From 813d8b7fb1a2ad4512a63cee457e4b1cff9d4285 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 17:04:47 +0900 Subject: [PATCH 01/49] feat: initial commit for vkemo relay as a copy of global timeline --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + packages/backend/src/server/ServerModule.ts | 2 + .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../endpoints/notes/vkemo-relay-timeline.ts | 113 +++++++++++++++++ .../src/server/api/stream/ChannelsService.ts | 3 + .../stream/channels/vkemo-relay-timeline.ts | 114 ++++++++++++++++++ .../frontend/src/components/MkTimeline.vue | 13 +- packages/frontend/src/pages/timeline.vue | 5 + packages/frontend/src/ui/deck/deck-store.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 3 + packages/misskey-js/src/streaming.types.ts | 10 ++ 13 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts create mode 100644 packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index c1aa163f9822..571ddf8b9883 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8569,6 +8569,10 @@ export interface Locale extends ILocale { * グローバル */ "global": string; + /** + * Virtual Kemomimi リレー + */ + "vkemoRelay": string; }; "_play": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 51380e49c523..0410fd2dee35 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2262,6 +2262,7 @@ _timelines: local: "ローカル" social: "ソーシャル" global: "グローバル" + vkemoRelay: "Virtual Kemomimi リレー" _play: new: "Playの作成" diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index f43968d236df..5c72084953c2 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -35,6 +35,7 @@ import { AntennaChannelService } from './api/stream/channels/antenna.js'; import { ChannelChannelService } from './api/stream/channels/channel.js'; import { DriveChannelService } from './api/stream/channels/drive.js'; import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js'; +import { VkemoRelayTimelineChannelService } from './api/stream/channels/vkemo-relay-timeline.js'; 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'; @@ -78,6 +79,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js ChannelChannelService, DriveChannelService, GlobalTimelineChannelService, + VkemoRelayTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, ReversiChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 88d3999eb0b1..cf8cbc6bda5a 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -272,6 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; 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_vkemoRelayTimeline from './endpoints/notes/vkemo-relay-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_mentions from './endpoints/notes/mentions.js'; @@ -644,6 +645,7 @@ const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create' const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; +const $notes_vkemoRelayTimeline: Provider = { provide: 'ep:notes/vkemo-relay-timeline', useClass: ep___notes_vkemoRelayTimeline.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_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; @@ -1020,6 +1022,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_delete, $notes_featured, $notes_globalTimeline, + $notes_vkemoRelayTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, @@ -1390,6 +1393,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_delete, $notes_featured, $notes_globalTimeline, + $notes_vkemoRelayTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f7e64a7356d4..48cdc783b79b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -272,6 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; 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_vkemoRelayTimeline from './endpoints/notes/vkemo-relay-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_mentions from './endpoints/notes/mentions.js'; @@ -642,6 +643,7 @@ const eps = [ ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], + ['notes/vkemo-relay-timeline', ep___notes_vkemoRelayTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], ['notes/mentions', ep___notes_mentions], diff --git a/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts new file mode 100644 index 000000000000..258a0bfb8f9d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.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: { + gtlDisabled: { + message: 'Global timeline has been disabled.', + code: 'GTL_DISABLED', + id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + withFiles: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, + 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; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private roleService: RoleService, + private activeUsersChart: ActiveUsersChart, + ) { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me ? me.id : null); + if (!policies.gtlAvailable) { + throw new ApiError(meta.errors.gtlDisabled); + } + + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.where('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.where('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + //#endregion + + const timeline = await query.limit(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 253409259fad..012ab45511c4 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -9,6 +9,7 @@ import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; import { LocalTimelineChannelService } from './channels/local-timeline.js'; import { HomeTimelineChannelService } from './channels/home-timeline.js'; import { GlobalTimelineChannelService } from './channels/global-timeline.js'; +import { VkemoRelayTimelineChannelService } from './channels/vkemo-relay-timeline.js'; import { MainChannelService } from './channels/main.js'; import { ChannelChannelService } from './channels/channel.js'; import { AdminChannelService } from './channels/admin.js'; @@ -31,6 +32,7 @@ export class ChannelsService { private localTimelineChannelService: LocalTimelineChannelService, private hybridTimelineChannelService: HybridTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, + private vkemoRelayTimelineChannelService: VkemoRelayTimelineChannelService, private userListChannelService: UserListChannelService, private hashtagChannelService: HashtagChannelService, private roleTimelineChannelService: RoleTimelineChannelService, @@ -53,6 +55,7 @@ export class ChannelsService { case 'localTimeline': return this.localTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; + case 'vkemoRelayTimeline': return this.vkemoRelayTimelineChannelService; case 'userList': return this.userListChannelService; case 'hashtag': return this.hashtagChannelService; case 'roleTimeline': return this.roleTimelineChannelService; diff --git a/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts new file mode 100644 index 000000000000..78bc5e504bd2 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { isInstanceMuted } from '@/misc/is-instance-muted.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, { type MiChannelService } from '../channel.js'; + +class VkemoRelayTimelineChannel extends Channel { + public readonly chName = 'vkemoRelayTimeline'; + public static shouldShare = false; + public static requireCredential = false as const; + private withRenotes: boolean; + private withFiles: boolean; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: any) { + const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); + if (!policies.gtlAvailable) return; + + this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @bindThis + private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + + if (note.visibility !== 'public') return; + if (note.channelId != null) return; + + // 関係ない返信は除外 + if (note.reply && !this.following[note.userId]?.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) 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; + + if (this.user && note.renoteId && !note.text) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } + } + + this.connection.cacheNote(note); + + this.send('note', note); + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} + +@Injectable() +export class VkemoRelayTimelineChannelService implements MiChannelService { + public readonly shouldShare = VkemoRelayTimelineChannel.shouldShare; + public readonly requireCredential = VkemoRelayTimelineChannel.requireCredential; + public readonly kind = VkemoRelayTimelineChannel.kind; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): VkemoRelayTimelineChannel { + return new VkemoRelayTimelineChannel( + 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 03dccb18e9ea..c48721f7f34b 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -29,7 +29,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: 'home' | 'local' | 'social' | 'global' | 'vkemo-relay' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; list?: string; antenna?: string; channel?: string; @@ -121,6 +121,11 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }); + } else if (props.src === 'vkemo-relay') { + connection = stream.useChannel('vkemoRelayTimeline', { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); connection.on('mention', prepend); @@ -193,6 +198,12 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }; + } else if (props.src === 'vkemo-relay') { + endpoint = 'notes/vkemo-relay-timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }; } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; query = null; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 48dfc1fd44fd..9ce5d3b1d304 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -305,6 +305,11 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList title: i18n.ts._timelines.global, icon: 'ti ti-whirl', iconOnly: true, +}, { + key: 'vkemo-relay', + title: i18n.ts._timelines.relay, + icon: 'ti ti-whirl', // TODO: update icon + iconOnly: true, }] : []), { icon: 'ti ti-list', title: i18n.ts.lists, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 70b55e8172a4..d5ef7d61c8ab 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -29,7 +29,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global'; + tl?: 'home' | 'local' | 'social' | 'global' | 'vkemo-relay'; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index f9066d9db760..aaede854cf63 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ column.name }} @@ -95,6 +96,8 @@ async function setType() { value: 'social' as const, text: i18n.ts._timelines.social, }, { value: 'global' as const, text: i18n.ts._timelines.global, + }, { + value: 'vkemo-relay' as const, text: i18n.ts._timelines.vkemoRelay, }], }); if (canceled) { diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 9a86e03d69f6..795b07444940 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -102,6 +102,16 @@ export type Channels = { }; receives: null; }; + vkemoRelayTimeline: { + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; userList: { params: { listId: string; From 287aecfcf50941cc823142ac9b585dd047e8cb17 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 17:42:42 +0900 Subject: [PATCH 02/49] feat: filter notes from non-vkemo servers --- packages/backend/src/core/CoreModule.ts | 7 +++ .../src/core/VkemoRelayTimelineService.ts | 59 +++++++++++++++++++ .../stream/channels/vkemo-relay-timeline.ts | 6 ++ 3 files changed, 72 insertions(+) create mode 100644 packages/backend/src/core/VkemoRelayTimelineService.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 2c27d33c0649..39cfb43913f7 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -140,8 +140,10 @@ import { QueueModule } from './QueueModule.js'; import { QueueService } from './QueueService.js'; import { LoggerService } from './LoggerService.js'; import type { Provider } from '@nestjs/common'; +import {VkemoRelayTimelineService} from "@/core/VkemoRelayTimelineService.js"; //#region 文字列ベースでのinjection用(循環参照対応のため) +const $VkemoRelayTimelineService: Provider = { provide: 'VkemoRelayTimelineService', useExisting: VkemoRelayTimelineService }; const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; @@ -282,6 +284,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueModule, ], providers: [ + VkemoRelayTimelineService, LoggerService, AccountMoveService, AccountUpdateService, @@ -418,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueService, //#region 文字列ベースでのinjection用(循環参照対応のため) + $VkemoRelayTimelineService, $LoggerService, $AccountMoveService, $AccountUpdateService, @@ -482,6 +486,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ChannelFollowingService, $RegistryApiService, $ReversiService, + $VkemoRelayTimelineService, $ChartLoggerService, $FederationChart, @@ -554,6 +559,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#endregion ], exports: [ + VkemoRelayTimelineService, QueueModule, LoggerService, AccountMoveService, @@ -690,6 +696,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueService, //#region 文字列ベースでのinjection用(循環参照対応のため) + $VkemoRelayTimelineService, $LoggerService, $AccountMoveService, $AccountUpdateService, diff --git a/packages/backend/src/core/VkemoRelayTimelineService.ts b/packages/backend/src/core/VkemoRelayTimelineService.ts new file mode 100644 index 000000000000..e499e024ea2f --- /dev/null +++ b/packages/backend/src/core/VkemoRelayTimelineService.ts @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class VkemoRelayTimelineService { + instanceHosts: Set; + + constructor() { + // TODO: fetch instance list from https://relay.virtualkemomimi.net/api/servers + this.instanceHosts = new Set([ + 'buicha.social', + 'misskey.niri.la', + 'metaskey.net', + 'virtualkemomimi.net', + 'kawaiivrc.site', + 'misskey.pm', + 'vrc-ins.net', + 'misskey.shunrin.com', + 'vrcjp.hostdon.ne.jp', + 'key.hinasense.jp', + 'mi.harumakizaemon.net', + 'misskey.emymin.net', + 'msky.summersweet.jp', + 'misskey.nokotaro.com', + 'mewl.me', + 'mstdn.virtecam.net', + 'inokashiraskey.jp', + 'misskey.kakunpc.com', + 'misskey.invr.chat', + 'superneko.net', + 'misskey.syuuta.net', + 'mi.nyaa.app', + 'nep.one', + 'meron.cloud', + 'misskey.tsukiyo.dev', + 'mi-x500.i-0.io', + 'misskey.yukkukomei.com', + 'itsukey.net', + 'misskey.narazaka.net', + 'misskey.meglia.dev', + 'misskey.makihiro.info', + 'misskey.ayatovr.dev', + 'atsuchan.page', + 'mochitter.net', + ]); + } + + @bindThis + isRelayedInstance(host: string | null): boolean { + // assuming the current instance is joined to the i relay + if (host == null) return true; + return this.instanceHosts.has(host); + } +} diff --git a/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts index 78bc5e504bd2..b517da0f16c1 100644 --- a/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts @@ -11,6 +11,7 @@ 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 { VkemoRelayTimelineService } from '@/core/VkemoRelayTimelineService.js'; import Channel, { type MiChannelService } from '../channel.js'; class VkemoRelayTimelineChannel extends Channel { @@ -24,6 +25,7 @@ class VkemoRelayTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private vkemoRelayTimelineService: VkemoRelayTimelineService, id: string, connection: Channel['connection'], @@ -59,6 +61,8 @@ class VkemoRelayTimelineChannel extends Channel { if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // Ignore notes from non-vkemo relay + if (!this.vkemoRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; @@ -98,6 +102,7 @@ export class VkemoRelayTimelineChannelService implements MiChannelService private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private vkemoRelayTimelineService: VkemoRelayTimelineService, ) { } @@ -107,6 +112,7 @@ export class VkemoRelayTimelineChannelService implements MiChannelService this.metaService, this.roleService, this.noteEntityService, + this.vkemoRelayTimelineService, id, connection, ); From 72766f7ec30c51e4eb3a740f79ea4a86885e6be2 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 18:02:53 +0900 Subject: [PATCH 03/49] feat: filter notes in endpoint version of vkemo relay --- .../backend/src/core/VkemoRelayTimelineService.ts | 14 ++++++++++++++ .../api/endpoints/notes/vkemo-relay-timeline.ts | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/VkemoRelayTimelineService.ts b/packages/backend/src/core/VkemoRelayTimelineService.ts index e499e024ea2f..71f3efa2340e 100644 --- a/packages/backend/src/core/VkemoRelayTimelineService.ts +++ b/packages/backend/src/core/VkemoRelayTimelineService.ts @@ -4,6 +4,7 @@ */ import { Injectable } from '@nestjs/common'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -56,4 +57,17 @@ export class VkemoRelayTimelineService { if (host == null) return true; return this.instanceHosts.has(host); } + + get hostNames (): string[] { + return Array.from(this.instanceHosts); + } + + @bindThis + generateFilterQuery(query: SelectQueryBuilder) { + query.andWhere(new Brackets(qb => { + qb + .andWhere('note.userHost IS NULL') + .orWhere('note.userHost IN (:...vkemoRelayInstances)', { vkemoRelayInstances: this.hostNames }); + })); + } } diff --git a/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts index 258a0bfb8f9d..1b639de4b9b5 100644 --- a/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: anatawa12 * SPDX-License-Identifier: AGPL-3.0-only */ @@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { VkemoRelayTimelineService } from '@/core/VkemoRelayTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -60,6 +61,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, + private vkemoRelayTimelineService: VkemoRelayTimelineService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); @@ -78,6 +80,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + this.vkemoRelayTimelineService.generateFilterQuery(query); + if (me) { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); From 8c4982542e7939331a64a88f01f64725b72b5a0d Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 18:39:49 +0900 Subject: [PATCH 04/49] feat: fetch instance list from API --- .../src/core/VkemoRelayTimelineService.ts | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/packages/backend/src/core/VkemoRelayTimelineService.ts b/packages/backend/src/core/VkemoRelayTimelineService.ts index 71f3efa2340e..164f38c944f7 100644 --- a/packages/backend/src/core/VkemoRelayTimelineService.ts +++ b/packages/backend/src/core/VkemoRelayTimelineService.ts @@ -6,60 +6,68 @@ import { Injectable } from '@nestjs/common'; import { Brackets, SelectQueryBuilder } from 'typeorm'; import { bindThis } from '@/decorators.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; + +type VkemoInstanceList = { Url: string; }[]; @Injectable() export class VkemoRelayTimelineService { instanceHosts: Set; + instanceHostsArray: string[]; + lastUpdated: number; + updatePromise: Promise | null; + private logger: Logger; + + constructor( + private httpRequestService: HttpRequestService, + private loggerService: LoggerService, + ) { + // Initialize with + this.instanceHosts = new Set([]); + this.instanceHostsArray = []; + this.lastUpdated = 0; + this.updatePromise = null; + + this.logger = this.loggerService.getLogger('vkemo'); + + this.checkForUpdateInstanceList(); + } - constructor() { - // TODO: fetch instance list from https://relay.virtualkemomimi.net/api/servers - this.instanceHosts = new Set([ - 'buicha.social', - 'misskey.niri.la', - 'metaskey.net', - 'virtualkemomimi.net', - 'kawaiivrc.site', - 'misskey.pm', - 'vrc-ins.net', - 'misskey.shunrin.com', - 'vrcjp.hostdon.ne.jp', - 'key.hinasense.jp', - 'mi.harumakizaemon.net', - 'misskey.emymin.net', - 'msky.summersweet.jp', - 'misskey.nokotaro.com', - 'mewl.me', - 'mstdn.virtecam.net', - 'inokashiraskey.jp', - 'misskey.kakunpc.com', - 'misskey.invr.chat', - 'superneko.net', - 'misskey.syuuta.net', - 'mi.nyaa.app', - 'nep.one', - 'meron.cloud', - 'misskey.tsukiyo.dev', - 'mi-x500.i-0.io', - 'misskey.yukkukomei.com', - 'itsukey.net', - 'misskey.narazaka.net', - 'misskey.meglia.dev', - 'misskey.makihiro.info', - 'misskey.ayatovr.dev', - 'atsuchan.page', - 'mochitter.net', - ]); + @bindThis + checkForUpdateInstanceList() { + // one day + const UpdateInterval = 60 * 60 * 24; + + if (this.updatePromise == null && this.lastUpdated + UpdateInterval < Date.now()) { + this.updatePromise = this.updateInstanceList().then(() => { + this.updatePromise = null; + }); + } + } + + @bindThis + async updateInstanceList() { + this.logger.info('Updating instance list'); + const instanceList = await this.httpRequestService.getJson('https://relay.virtualkemomimi.net/api/servers'); + this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host); + this.instanceHosts = new Set(this.instanceHostsArray); + this.lastUpdated = Date.now(); + this.logger.info(`Got instance list: ${this.instanceHostsArray}`); } @bindThis isRelayedInstance(host: string | null): boolean { + this.checkForUpdateInstanceList(); // assuming the current instance is joined to the i relay if (host == null) return true; return this.instanceHosts.has(host); } get hostNames (): string[] { - return Array.from(this.instanceHosts); + this.checkForUpdateInstanceList(); + return this.instanceHostsArray; } @bindThis From 242f503beed165497040e5f2ca27349fb0fb1df2 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 18:57:39 +0900 Subject: [PATCH 05/49] chore: rename vkemo -> vmimi --- locales/index.d.ts | 2 +- locales/ja-JP.yml | 2 +- packages/backend/src/core/CoreModule.ts | 13 ++++----- ...ervice.ts => VmimiRelayTimelineService.ts} | 10 +++---- packages/backend/src/server/ServerModule.ts | 4 +-- .../backend/src/server/api/EndpointsModule.ts | 8 +++--- packages/backend/src/server/api/endpoints.ts | 4 +-- ...ay-timeline.ts => vmimi-relay-timeline.ts} | 6 ++-- .../src/server/api/stream/ChannelsService.ts | 6 ++-- ...ay-timeline.ts => vmimi-relay-timeline.ts} | 28 +++++++++---------- .../frontend/src/components/MkTimeline.vue | 10 +++---- packages/frontend/src/pages/timeline.vue | 2 +- packages/frontend/src/ui/deck/deck-store.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 4 +-- packages/misskey-js/src/streaming.types.ts | 2 +- 15 files changed, 51 insertions(+), 52 deletions(-) rename packages/backend/src/core/{VkemoRelayTimelineService.ts => VmimiRelayTimelineService.ts} (84%) rename packages/backend/src/server/api/endpoints/notes/{vkemo-relay-timeline.ts => vmimi-relay-timeline.ts} (94%) rename packages/backend/src/server/api/stream/channels/{vkemo-relay-timeline.ts => vmimi-relay-timeline.ts} (82%) diff --git a/locales/index.d.ts b/locales/index.d.ts index 571ddf8b9883..f89e2e0b6c40 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8572,7 +8572,7 @@ export interface Locale extends ILocale { /** * Virtual Kemomimi リレー */ - "vkemoRelay": string; + "vmimiRelay": string; }; "_play": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0410fd2dee35..34cfb144ff71 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2262,7 +2262,7 @@ _timelines: local: "ローカル" social: "ソーシャル" global: "グローバル" - vkemoRelay: "Virtual Kemomimi リレー" + vmimiRelay: "Virtual Kemomimi リレー" _play: new: "Playの作成" diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 39cfb43913f7..506bea8c0cb9 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -136,14 +136,14 @@ import { ApMentionService } from './activitypub/models/ApMentionService.js'; import { ApNoteService } from './activitypub/models/ApNoteService.js'; import { ApPersonService } from './activitypub/models/ApPersonService.js'; import { ApQuestionService } from './activitypub/models/ApQuestionService.js'; +import { VmimiRelayTimelineService } from './VmimiRelayTimelineService.js'; import { QueueModule } from './QueueModule.js'; import { QueueService } from './QueueService.js'; import { LoggerService } from './LoggerService.js'; import type { Provider } from '@nestjs/common'; -import {VkemoRelayTimelineService} from "@/core/VkemoRelayTimelineService.js"; //#region 文字列ベースでのinjection用(循環参照対応のため) -const $VkemoRelayTimelineService: Provider = { provide: 'VkemoRelayTimelineService', useExisting: VkemoRelayTimelineService }; +const $VmimiRelayTimelineService: Provider = { provide: 'VmimiRelayTimelineService', useExisting: VmimiRelayTimelineService }; const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; @@ -284,7 +284,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueModule, ], providers: [ - VkemoRelayTimelineService, + VmimiRelayTimelineService, LoggerService, AccountMoveService, AccountUpdateService, @@ -421,7 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueService, //#region 文字列ベースでのinjection用(循環参照対応のため) - $VkemoRelayTimelineService, + $VmimiRelayTimelineService, $LoggerService, $AccountMoveService, $AccountUpdateService, @@ -486,7 +486,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ChannelFollowingService, $RegistryApiService, $ReversiService, - $VkemoRelayTimelineService, $ChartLoggerService, $FederationChart, @@ -559,7 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#endregion ], exports: [ - VkemoRelayTimelineService, + VmimiRelayTimelineService, QueueModule, LoggerService, AccountMoveService, @@ -696,7 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueService, //#region 文字列ベースでのinjection用(循環参照対応のため) - $VkemoRelayTimelineService, + $VmimiRelayTimelineService, $LoggerService, $AccountMoveService, $AccountUpdateService, diff --git a/packages/backend/src/core/VkemoRelayTimelineService.ts b/packages/backend/src/core/VmimiRelayTimelineService.ts similarity index 84% rename from packages/backend/src/core/VkemoRelayTimelineService.ts rename to packages/backend/src/core/VmimiRelayTimelineService.ts index 164f38c944f7..9772b5eda091 100644 --- a/packages/backend/src/core/VkemoRelayTimelineService.ts +++ b/packages/backend/src/core/VmimiRelayTimelineService.ts @@ -10,10 +10,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -type VkemoInstanceList = { Url: string; }[]; +type VmimiInstanceList = { Url: string; }[]; @Injectable() -export class VkemoRelayTimelineService { +export class VmimiRelayTimelineService { instanceHosts: Set; instanceHostsArray: string[]; lastUpdated: number; @@ -30,7 +30,7 @@ export class VkemoRelayTimelineService { this.lastUpdated = 0; this.updatePromise = null; - this.logger = this.loggerService.getLogger('vkemo'); + this.logger = this.loggerService.getLogger('vmimi'); this.checkForUpdateInstanceList(); } @@ -50,7 +50,7 @@ export class VkemoRelayTimelineService { @bindThis async updateInstanceList() { this.logger.info('Updating instance list'); - const instanceList = await this.httpRequestService.getJson('https://relay.virtualkemomimi.net/api/servers'); + const instanceList = await this.httpRequestService.getJson('https://relay.virtualkemomimi.net/api/servers'); this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host); this.instanceHosts = new Set(this.instanceHostsArray); this.lastUpdated = Date.now(); @@ -75,7 +75,7 @@ export class VkemoRelayTimelineService { query.andWhere(new Brackets(qb => { qb .andWhere('note.userHost IS NULL') - .orWhere('note.userHost IN (:...vkemoRelayInstances)', { vkemoRelayInstances: this.hostNames }); + .orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: this.hostNames }); })); } } diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 5c72084953c2..711b28e50746 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -35,7 +35,7 @@ import { AntennaChannelService } from './api/stream/channels/antenna.js'; import { ChannelChannelService } from './api/stream/channels/channel.js'; import { DriveChannelService } from './api/stream/channels/drive.js'; import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js'; -import { VkemoRelayTimelineChannelService } from './api/stream/channels/vkemo-relay-timeline.js'; +import { VmimiRelayTimelineChannelService } from './api/stream/channels/vmimi-relay-timeline.js'; 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'; @@ -79,7 +79,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js ChannelChannelService, DriveChannelService, GlobalTimelineChannelService, - VkemoRelayTimelineChannelService, + VmimiRelayTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, ReversiChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index cf8cbc6bda5a..827c9e66b7ce 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -272,7 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; 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_vkemoRelayTimeline from './endpoints/notes/vkemo-relay-timeline.js'; +import * as ep___notes_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-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_mentions from './endpoints/notes/mentions.js'; @@ -645,7 +645,7 @@ const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create' const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; -const $notes_vkemoRelayTimeline: Provider = { provide: 'ep:notes/vkemo-relay-timeline', useClass: ep___notes_vkemoRelayTimeline.default }; +const $notes_vmimiRelayTimeline: Provider = { provide: 'ep:notes/vmimi-relay-timeline', useClass: ep___notes_vmimiRelayTimeline.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_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; @@ -1022,7 +1022,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_delete, $notes_featured, $notes_globalTimeline, - $notes_vkemoRelayTimeline, + $notes_vmimiRelayTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, @@ -1393,7 +1393,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_delete, $notes_featured, $notes_globalTimeline, - $notes_vkemoRelayTimeline, + $notes_vmimiRelayTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 48cdc783b79b..28e7ef58c25f 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -272,7 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; 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_vkemoRelayTimeline from './endpoints/notes/vkemo-relay-timeline.js'; +import * as ep___notes_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-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_mentions from './endpoints/notes/mentions.js'; @@ -643,7 +643,7 @@ const eps = [ ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], - ['notes/vkemo-relay-timeline', ep___notes_vkemoRelayTimeline], + ['notes/vmimi-relay-timeline', ep___notes_vmimiRelayTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], ['notes/mentions', ep___notes_mentions], diff --git a/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts similarity index 94% rename from packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts rename to packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index 1b639de4b9b5..c8fe1ded57b5 100644 --- a/packages/backend/src/server/api/endpoints/notes/vkemo-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -12,7 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; -import { VkemoRelayTimelineService } from '@/core/VkemoRelayTimelineService.js'; +import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -61,7 +61,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, - private vkemoRelayTimelineService: VkemoRelayTimelineService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); @@ -80,7 +80,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.vkemoRelayTimelineService.generateFilterQuery(query); + this.vmimiRelayTimelineService.generateFilterQuery(query); if (me) { this.queryService.generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 012ab45511c4..4223a296767a 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -9,7 +9,7 @@ import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; import { LocalTimelineChannelService } from './channels/local-timeline.js'; import { HomeTimelineChannelService } from './channels/home-timeline.js'; import { GlobalTimelineChannelService } from './channels/global-timeline.js'; -import { VkemoRelayTimelineChannelService } from './channels/vkemo-relay-timeline.js'; +import { VmimiRelayTimelineChannelService } from './channels/vmimi-relay-timeline.js'; import { MainChannelService } from './channels/main.js'; import { ChannelChannelService } from './channels/channel.js'; import { AdminChannelService } from './channels/admin.js'; @@ -32,7 +32,7 @@ export class ChannelsService { private localTimelineChannelService: LocalTimelineChannelService, private hybridTimelineChannelService: HybridTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, - private vkemoRelayTimelineChannelService: VkemoRelayTimelineChannelService, + private vmimiRelayTimelineChannelService: VmimiRelayTimelineChannelService, private userListChannelService: UserListChannelService, private hashtagChannelService: HashtagChannelService, private roleTimelineChannelService: RoleTimelineChannelService, @@ -55,7 +55,7 @@ export class ChannelsService { case 'localTimeline': return this.localTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; - case 'vkemoRelayTimeline': return this.vkemoRelayTimelineChannelService; + case 'vmimiRelayTimeline': return this.vmimiRelayTimelineChannelService; case 'userList': return this.userListChannelService; case 'hashtag': return this.hashtagChannelService; case 'roleTimeline': return this.roleTimelineChannelService; diff --git a/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts similarity index 82% rename from packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts rename to packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index b517da0f16c1..0873e73fd4d2 100644 --- a/packages/backend/src/server/api/stream/channels/vkemo-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -11,11 +11,11 @@ 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 { VkemoRelayTimelineService } from '@/core/VkemoRelayTimelineService.js'; +import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; import Channel, { type MiChannelService } from '../channel.js'; -class VkemoRelayTimelineChannel extends Channel { - public readonly chName = 'vkemoRelayTimeline'; +class VmimiRelayTimelineChannel extends Channel { + public readonly chName = 'vmimiRelayTimeline'; public static shouldShare = false; public static requireCredential = false as const; private withRenotes: boolean; @@ -25,7 +25,7 @@ class VkemoRelayTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private vkemoRelayTimelineService: VkemoRelayTimelineService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, id: string, connection: Channel['connection'], @@ -61,8 +61,8 @@ class VkemoRelayTimelineChannel extends Channel { if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; - // Ignore notes from non-vkemo relay - if (!this.vkemoRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; + // Ignore notes from non-vmimi relay + if (!this.vmimiRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; @@ -93,26 +93,26 @@ class VkemoRelayTimelineChannel extends Channel { } @Injectable() -export class VkemoRelayTimelineChannelService implements MiChannelService { - public readonly shouldShare = VkemoRelayTimelineChannel.shouldShare; - public readonly requireCredential = VkemoRelayTimelineChannel.requireCredential; - public readonly kind = VkemoRelayTimelineChannel.kind; +export class VmimiRelayTimelineChannelService implements MiChannelService { + public readonly shouldShare = VmimiRelayTimelineChannel.shouldShare; + public readonly requireCredential = VmimiRelayTimelineChannel.requireCredential; + public readonly kind = VmimiRelayTimelineChannel.kind; constructor( private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private vkemoRelayTimelineService: VkemoRelayTimelineService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, ) { } @bindThis - public create(id: string, connection: Channel['connection']): VkemoRelayTimelineChannel { - return new VkemoRelayTimelineChannel( + public create(id: string, connection: Channel['connection']): VmimiRelayTimelineChannel { + return new VmimiRelayTimelineChannel( this.metaService, this.roleService, this.noteEntityService, - this.vkemoRelayTimelineService, + this.vmimiRelayTimelineService, id, connection, ); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index c48721f7f34b..8ca55f53ecd6 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -29,7 +29,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'global' | 'vkemo-relay' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; list?: string; antenna?: string; channel?: string; @@ -121,8 +121,8 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }); - } else if (props.src === 'vkemo-relay') { - connection = stream.useChannel('vkemoRelayTimeline', { + } else if (props.src === 'vmimi-relay') { + connection = stream.useChannel('vmimiRelayTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }); @@ -198,8 +198,8 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }; - } else if (props.src === 'vkemo-relay') { - endpoint = 'notes/vkemo-relay-timeline'; + } else if (props.src === 'vmimi-relay') { + endpoint = 'notes/vmimi-relay-timeline'; query = { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 9ce5d3b1d304..49da8fa82d2c 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -306,7 +306,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList icon: 'ti ti-whirl', iconOnly: true, }, { - key: 'vkemo-relay', + key: 'vmimi-relay', title: i18n.ts._timelines.relay, icon: 'ti ti-whirl', // TODO: update icon iconOnly: true, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index d5ef7d61c8ab..a41b5ecb28c5 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -29,7 +29,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global' | 'vkemo-relay'; + tl?: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay'; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index aaede854cf63..1de558cd76d7 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + {{ column.name }} @@ -97,7 +97,7 @@ async function setType() { }, { value: 'global' as const, text: i18n.ts._timelines.global, }, { - value: 'vkemo-relay' as const, text: i18n.ts._timelines.vkemoRelay, + value: 'vmimi-relay' as const, text: i18n.ts._timelines.vmimiRelay, }], }); if (canceled) { diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 795b07444940..3209f7dfa991 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -102,7 +102,7 @@ export type Channels = { }; receives: null; }; - vkemoRelayTimeline: { + vmimiRelayTimeline: { params: { withRenotes?: boolean; withFiles?: boolean; From 9f607cb51bbc0003e8f883cb66df31ee76ea2a71 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 19:04:43 +0900 Subject: [PATCH 06/49] chore: update misskey-js --- packages/misskey-js/etc/misskey-js.api.md | 18 +++++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 +++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 73 +++++++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 2237d278f46f..13702ffab85b 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -592,6 +592,16 @@ export type Channels = { }; receives: null; }; + vmimiRelayTimeline: { + params: { + withRenotes?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; userList: { params: { listId: string; @@ -1518,6 +1528,8 @@ declare namespace entities { NotesFeaturedResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, + NotesVmimiRelayTimelineRequest, + NotesVmimiRelayTimelineResponse, NotesHybridTimelineRequest, NotesHybridTimelineResponse, NotesLocalTimelineRequest, @@ -2564,6 +2576,12 @@ type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requ // @public (undocumented) type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesVmimiRelayTimelineRequest = operations['notes/vmimi-relay-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesVmimiRelayTimelineResponse = operations['notes/vmimi-relay-timeline']['responses']['200']['content']['application/json']; + // @public (undocumented) export const noteVisibilities: readonly ["public", "home", "followers", "specified"]; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 53093501008c..7a58b3eb0401 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -2964,6 +2964,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index b0982e1e55d0..675511cf47f3 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -395,6 +395,8 @@ import type { NotesFeaturedResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, + NotesVmimiRelayTimelineRequest, + NotesVmimiRelayTimelineResponse, NotesHybridTimelineRequest, NotesHybridTimelineResponse, NotesLocalTimelineRequest, @@ -820,6 +822,7 @@ export type Endpoints = { 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; 'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse }; + 'notes/vmimi-relay-timeline': { req: NotesVmimiRelayTimelineRequest; res: NotesVmimiRelayTimelineResponse }; 'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse }; 'notes/local-timeline': { req: NotesLocalTimelineRequest; res: NotesLocalTimelineResponse }; 'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a936931e99e8..57ad9ce4bc02 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -397,6 +397,8 @@ export type NotesFeaturedRequest = operations['notes/featured']['requestBody'][' export type NotesFeaturedResponse = operations['notes/featured']['responses']['200']['content']['application/json']; export type NotesGlobalTimelineRequest = operations['notes/global-timeline']['requestBody']['content']['application/json']; export type NotesGlobalTimelineResponse = operations['notes/global-timeline']['responses']['200']['content']['application/json']; +export type NotesVmimiRelayTimelineRequest = operations['notes/vmimi-relay-timeline']['requestBody']['content']['application/json']; +export type NotesVmimiRelayTimelineResponse = operations['notes/vmimi-relay-timeline']['responses']['200']['content']['application/json']; export type NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; export type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['content']['application/json']; export type NotesLocalTimelineRequest = operations['notes/local-timeline']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index b1e6a194f9df..a5b458d9daa9 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2574,6 +2574,15 @@ export type paths = { */ post: operations['notes/global-timeline']; }; + '/notes/vmimi-relay-timeline': { + /** + * notes/vmimi-relay-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['notes/vmimi-relay-timeline']; + }; '/notes/hybrid-timeline': { /** * notes/hybrid-timeline @@ -20773,6 +20782,70 @@ export type operations = { }; }; }; + /** + * notes/vmimi-relay-timeline + * @description No description provided. + * + * **Credential required**: *No* + */ + 'notes/vmimi-relay-timeline': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/hybrid-timeline * @description No description provided. From aecac190edae56fce8d5b0262cc637358df099b2 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 19:55:57 +0900 Subject: [PATCH 07/49] chore: set icon for vmimi relay tl --- packages/frontend/src/pages/timeline.vue | 10 +++++----- packages/frontend/src/store.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 49da8fa82d2c..f2decacfd7b2 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -67,7 +67,7 @@ const rootEl = shallowRef(); const queue = ref(0); const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global'); -const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({ +const src = computed<'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`>({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), }); @@ -199,7 +199,7 @@ async function chooseChannel(ev: MouseEvent): Promise { os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void { +function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`): void { const out = deepMerge({ src: newSrc }, defaultStore.state.tl); if (newSrc.startsWith('userList:')) { @@ -307,8 +307,8 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList iconOnly: true, }, { key: 'vmimi-relay', - title: i18n.ts._timelines.relay, - icon: 'ti ti-whirl', // TODO: update icon + title: i18n.ts._timelines.vmimiRelay, + icon: 'ti ti-circles-relation', iconOnly: true, }] : []), { icon: 'ti ti-list', @@ -344,7 +344,7 @@ const headerTabsWhenNotLogin = computed(() => [ definePageMetadata(() => ({ title: i18n.ts.timeline, - icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : 'ti ti-home', + icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : src.value === 'vmimi-relay' ? 'ti ti-circles-relation' : 'ti ti-home', })); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index dfc4169a54bc..516fc296a18a 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -184,7 +184,7 @@ export const defaultStore = markRaw(new Storage('base', { tl: { where: 'deviceAccount', default: { - src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`, + src: 'home' as 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`, userList: null as Misskey.entities.UserList | null, filter: { withReplies: true, diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 1de558cd76d7..05653d094fba 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + {{ column.name }} @@ -107,7 +107,7 @@ async function setType() { return; } updateColumn(props.column.id, { - tl: src, + tl: src ?? undefined, }); } From ce06c805ca86b5cfdef61c3db1b7bf5ffbdbf0f3 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 20:28:29 +0900 Subject: [PATCH 08/49] =?UTF-8?q?chore(i18n):=20=E3=81=B6=E3=81=84?= =?UTF-8?q?=E3=81=BF=E3=81=BF=E3=83=AA=E3=83=AC=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- locales/ja-JP.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 34cfb144ff71..61f2db9f831e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2262,7 +2262,7 @@ _timelines: local: "ローカル" social: "ソーシャル" global: "グローバル" - vmimiRelay: "Virtual Kemomimi リレー" + vmimiRelay: "ぶいみみリレー" _play: new: "Playの作成" From 5f79f2dc78e9610f0a7bf7df2db6a19592e9c9fa Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 21:44:41 +0900 Subject: [PATCH 09/49] chore: retry when error returned from relay server --- .../src/core/VmimiRelayTimelineService.ts | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/core/VmimiRelayTimelineService.ts b/packages/backend/src/core/VmimiRelayTimelineService.ts index 9772b5eda091..4d120b362bb5 100644 --- a/packages/backend/src/core/VmimiRelayTimelineService.ts +++ b/packages/backend/src/core/VmimiRelayTimelineService.ts @@ -12,11 +12,15 @@ import type Logger from '@/logger.js'; type VmimiInstanceList = { Url: string; }[]; +// one day +const UpdateInterval = 1000 * 60 * 60 * 24; +const RetryInterval = 1000 * 60 * 60 * 6; + @Injectable() export class VmimiRelayTimelineService { instanceHosts: Set; instanceHostsArray: string[]; - lastUpdated: number; + nextUpdate: number; updatePromise: Promise | null; private logger: Logger; @@ -27,7 +31,7 @@ export class VmimiRelayTimelineService { // Initialize with this.instanceHosts = new Set([]); this.instanceHostsArray = []; - this.lastUpdated = 0; + this.nextUpdate = 0; this.updatePromise = null; this.logger = this.loggerService.getLogger('vmimi'); @@ -37,24 +41,25 @@ export class VmimiRelayTimelineService { @bindThis checkForUpdateInstanceList() { - // one day - const UpdateInterval = 60 * 60 * 24; - - if (this.updatePromise == null && this.lastUpdated + UpdateInterval < Date.now()) { - this.updatePromise = this.updateInstanceList().then(() => { - this.updatePromise = null; - }); + if (this.updatePromise == null && this.nextUpdate < Date.now()) { + this.updatePromise = this.updateInstanceList().finally(() => this.updatePromise = null); } } @bindThis async updateInstanceList() { - this.logger.info('Updating instance list'); - const instanceList = await this.httpRequestService.getJson('https://relay.virtualkemomimi.net/api/servers'); - this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host); - this.instanceHosts = new Set(this.instanceHostsArray); - this.lastUpdated = Date.now(); - this.logger.info(`Got instance list: ${this.instanceHostsArray}`); + try { + this.logger.info('Updating instance list'); + const instanceList = await this.httpRequestService.getJson('https://relay.virtualkemomimi.net/api/servers'); + this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host); + this.instanceHosts = new Set(this.instanceHostsArray); + this.nextUpdate = Date.now() + UpdateInterval; + this.logger.info(`Got instance list: ${this.instanceHostsArray}`); + } catch (e) { + this.logger.error('Failed to update instance list', e as any); + this.nextUpdate = Date.now() + RetryInterval; + setTimeout(() => this.checkForUpdateInstanceList(), RetryInterval + 5); + } } @bindThis @@ -73,9 +78,11 @@ export class VmimiRelayTimelineService { @bindThis generateFilterQuery(query: SelectQueryBuilder) { query.andWhere(new Brackets(qb => { - qb - .andWhere('note.userHost IS NULL') - .orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: this.hostNames }); + qb.where('note.userHost IS NULL'); + const names = this.hostNames; + if (names.length !== 0) { + qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names }); + } })); } } From 312340c7f6463035bb1995856ec1d8d5660502ca Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 11 Apr 2024 23:29:43 +0900 Subject: [PATCH 10/49] feat: withReplies for vmimi relay timeline --- locales/index.d.ts | 2 +- .../backend/src/core/VmimiRelayTimelineService.ts | 12 ++++++++++-- .../api/endpoints/notes/vmimi-relay-timeline.ts | 3 ++- .../api/stream/channels/vmimi-relay-timeline.ts | 5 +++++ packages/frontend/src/components/MkTimeline.vue | 2 ++ packages/frontend/src/pages/timeline.vue | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 2 +- packages/misskey-js/etc/misskey-js.api.md | 1 + packages/misskey-js/src/autogen/types.ts | 2 ++ packages/misskey-js/src/streaming.types.ts | 1 + 10 files changed, 26 insertions(+), 6 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index f89e2e0b6c40..a51d35437a66 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8570,7 +8570,7 @@ export interface Locale extends ILocale { */ "global": string; /** - * Virtual Kemomimi リレー + * ぶいみみリレー */ "vmimiRelay": string; }; diff --git a/packages/backend/src/core/VmimiRelayTimelineService.ts b/packages/backend/src/core/VmimiRelayTimelineService.ts index 4d120b362bb5..64604a19eeaa 100644 --- a/packages/backend/src/core/VmimiRelayTimelineService.ts +++ b/packages/backend/src/core/VmimiRelayTimelineService.ts @@ -76,13 +76,21 @@ export class VmimiRelayTimelineService { } @bindThis - generateFilterQuery(query: SelectQueryBuilder) { + generateFilterQuery(query: SelectQueryBuilder, excludeReplies: boolean) { + const names = this.hostNames; query.andWhere(new Brackets(qb => { qb.where('note.userHost IS NULL'); - const names = this.hostNames; if (names.length !== 0) { qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names }); } })); + if (excludeReplies) { + query.andWhere(new Brackets(qb => { + qb.where('note.replyUserHost IS NULL'); + if (names.length !== 0) { + qb.orWhere('note.replyUserHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names }); + } + })); + } } } diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index c8fe1ded57b5..de2453577abd 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -42,6 +42,7 @@ export const paramDef = { properties: { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withReplies: { 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' }, @@ -80,7 +81,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.vmimiRelayTimelineService.generateFilterQuery(query); + this.vmimiRelayTimelineService.generateFilterQuery(query, !ps.withReplies); if (me) { this.queryService.generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index 0873e73fd4d2..ef43b004d999 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -20,6 +20,7 @@ class VmimiRelayTimelineChannel extends Channel { public static requireCredential = false as const; private withRenotes: boolean; private withFiles: boolean; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -40,6 +41,7 @@ class VmimiRelayTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; + this.withReplies = params.withReplies ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -63,6 +65,9 @@ class VmimiRelayTimelineChannel extends Channel { // Ignore notes from non-vmimi relay if (!this.vmimiRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; + if (!this.withReplies && note.reply) { + if (!this.vmimiRelayTimelineService.isRelayedInstance(note.reply.user.host ?? null)) return; + } // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 8ca55f53ecd6..a1c1ea234118 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -125,6 +125,7 @@ function connectChannel() { connection = stream.useChannel('vmimiRelayTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, + withReplies: props.withReplies, }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); @@ -203,6 +204,7 @@ function updatePaginationQuery() { query = { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, + withReplies: props.withReplies, }; } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index f2decacfd7b2..5003e25a2c81 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -250,7 +250,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, src.value === 'local' || src.value === 'social' ? { + }, src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 05653d094fba..0090f4fd694f 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -119,7 +119,7 @@ const menu = [{ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, -}, props.column.tl === 'local' || props.column.tl === 'social' ? { +}, props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 13702ffab85b..8b1e8ddfc94f 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -596,6 +596,7 @@ export type Channels = { params: { withRenotes?: boolean; withFiles?: boolean; + withReplies?: boolean; }; events: { note: (payload: Note) => void; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index a5b458d9daa9..d8c12453eb19 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -20796,6 +20796,8 @@ export type operations = { withFiles?: boolean; /** @default true */ withRenotes?: boolean; + /** @default false */ + withReplies?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 3209f7dfa991..5374d8e1a5a7 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -106,6 +106,7 @@ export type Channels = { params: { withRenotes?: boolean; withFiles?: boolean; + withReplies?: boolean; }; events: { note: (payload: Note) => void; From 613a6f878de1493947953a1b1172b44c324a8ec0 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 12 Apr 2024 18:08:31 +0900 Subject: [PATCH 11/49] fix: if withReplies is false for the user in following, note is removed from vmimi-relay-timeline --- .../server/api/stream/channels/vmimi-relay-timeline.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index ef43b004d999..6ba8558dbaac 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -54,13 +54,6 @@ class VmimiRelayTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null) return; - // 関係ない返信は除外 - if (note.reply && !this.following[note.userId]?.withReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; - } - if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; // Ignore notes from non-vmimi relay @@ -68,9 +61,9 @@ class VmimiRelayTimelineChannel extends Channel { if (!this.withReplies && note.reply) { if (!this.vmimiRelayTimelineService.isRelayedInstance(note.reply.user.host ?? null)) return; } + // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する From 1c711115b4a6aaf2f4384a0aa4284de5dcfed2f5 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Mon, 15 Apr 2024 19:48:05 +0900 Subject: [PATCH 12/49] chore(vmimi-relay): reimplement with FFT --- .../1713168415416-VmimiRelayTimeline.js | 16 ++ .../backend/src/core/FanoutTimelineService.ts | 5 + .../backend/src/core/NoteCreateService.ts | 11 ++ packages/backend/src/core/RoleService.ts | 3 + packages/backend/src/models/Meta.ts | 5 + .../backend/src/models/json-schema/role.ts | 4 + .../src/server/api/endpoints/admin/meta.ts | 1 + .../server/api/endpoints/admin/update-meta.ts | 5 + .../endpoints/notes/vmimi-relay-timeline.ts | 146 +++++++++++++----- .../stream/channels/vmimi-relay-timeline.ts | 2 +- packages/frontend/src/pages/timeline.vue | 6 +- packages/misskey-js/src/autogen/types.ts | 4 + 12 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 packages/backend/migration/1713168415416-VmimiRelayTimeline.js diff --git a/packages/backend/migration/1713168415416-VmimiRelayTimeline.js b/packages/backend/migration/1713168415416-VmimiRelayTimeline.js new file mode 100644 index 000000000000..edc309aee481 --- /dev/null +++ b/packages/backend/migration/1713168415416-VmimiRelayTimeline.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: anatawa12 and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class VmimiRelayTimeline1713168415416 { + name = 'VmimiRelayTimeline1713168415416' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "vmimiRelayTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "vmimiRelayTimelineCacheMax"`); + } +} diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index f6dabfadcd6d..4edd4cf2a157 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -38,6 +38,11 @@ export type FanoutTimelineName = // role timelines | `roleTimeline:${string}` // any notes are included + // vmimi relay timelines + | 'vmimiRelayTimeline' // replies are not included + | 'vmimiRelayTimelineWithFiles' // only non-reply notes with files are included + | 'vmimiRelayTimelineWithReplies' // only replies are included + @Injectable() export class FanoutTimelineService { constructor( diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 81ae2908d3dc..df2e3f858dc3 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -50,6 +50,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; @@ -193,6 +194,7 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, + private vmimiRelayTimelineService: VmimiRelayTimelineService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -939,6 +941,9 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); } } + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + this.fanoutTimelineService.push('vmimiRelayTimelineWithReplies', note.id, meta.vmimiRelayTimelineCacheMax, r); + } } else { this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { @@ -951,6 +956,12 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + this.fanoutTimelineService.push('vmimiRelayTimeline', note.id, meta.vmimiRelayTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.fanoutTimelineService.push('vmimiRelayTimelineWithFiles', note.id, meta.vmimiRelayTimelineCacheMax / 2, r); + } + } } if (Math.random() < 0.1) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 09f309711445..2aaa66561f40 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -32,6 +32,7 @@ import { NotificationService } from '@/core/NotificationService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; export type RolePolicies = { + vrtlAvailable: boolean; gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; @@ -60,6 +61,7 @@ export type RolePolicies = { }; export const DEFAULT_POLICIES: RolePolicies = { + vrtlAvailable: true, gtlAvailable: true, ltlAvailable: true, canPublicNote: true, @@ -327,6 +329,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } return { + vrtlAvailable: calc('vrtlAvailable', vs => vs.some(v => v === true)), gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 66f19ce1975c..65c1c6eb7ec8 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -554,6 +554,11 @@ export class MiMeta { }) public preservedUsernames: string[]; + @Column('integer', { + default: 300, + }) + public vmimiRelayTimelineCacheMax: number; + @Column('boolean', { default: true, }) diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index c7702505039d..ceecbe79b699 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -151,6 +151,10 @@ export const packedRolePoliciesSchema = { type: 'object', optional: false, nullable: false, properties: { + vrtlAvailable: { + type: 'boolean', + optional: false, nullable: false, + }, gtlAvailable: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 88c5907bcc80..4be463a1fa0d 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -570,6 +570,7 @@ export default class extends Endpoint { // eslint- bannedEmailDomains: instance.bannedEmailDomains, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, + vmimiRelayTimelineCacheMax: instance.vmimiRelayTimelineCacheMax, enableFanoutTimeline: instance.enableFanoutTimeline, enableFanoutTimelineDbFallback: instance.enableFanoutTimelineDbFallback, perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index bffceef8151d..2be40d781590 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -138,6 +138,7 @@ export const paramDef = { manifestJsonOverride: { type: 'string' }, enableFanoutTimeline: { type: 'boolean' }, enableFanoutTimelineDbFallback: { type: 'boolean' }, + vmimiRelayTimelineCacheMax: { type: 'integer' }, perLocalUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' }, @@ -561,6 +562,10 @@ export default class extends Endpoint { // eslint- set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; } + if (ps.vmimiRelayTimelineCacheMax !== undefined) { + set.vmimiRelayTimelineCacheMax = ps.vmimiRelayTimelineCacheMax; + } + if (ps.perRemoteUserUserTimelineCacheMax !== undefined) { set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax; } diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index de2453577abd..fa50bae677e1 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -13,6 +13,10 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { MiLocalUser } from '@/models/User.js'; +import { MetaService } from '@/core/MetaService.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -29,10 +33,15 @@ export const meta = { }, errors: { - gtlDisabled: { - message: 'Global timeline has been disabled.', - code: 'GTL_DISABLED', - id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', + vmimiRelayDisabled: { + message: 'Vmimi Relay timeline has been disabled.', + code: 'VMIMI_RELAY_DISABLED', + id: '7f0064c3-59a0-4154-8c37-a8898c128ccc', + }, + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', }, }, } as const; @@ -44,6 +53,7 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, @@ -62,49 +72,65 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, + private idService: IdService, private vmimiRelayTimelineService: VmimiRelayTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + const policies = await this.roleService.getUserPolicies(me ? me.id : null); - if (!policies.gtlAvailable) { - throw new ApiError(meta.errors.gtlDisabled); + if (!policies.vrtlAvailable) { + throw new ApiError(meta.errors.vmimiRelayDisabled); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.visibility = \'public\'') - .andWhere('note.channelId IS NULL') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - this.vmimiRelayTimelineService.generateFilterQuery(query, !ps.withReplies); - - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + const serverSettings = await this.metaService.fetch(); - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.where('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.where('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); + if (!serverSettings.enableFanoutTimeline) { + const timeline = await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + withReplies: ps.withReplies, + }, me); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(timeline, me); } - //#endregion - const timeline = await query.limit(ps.limit).getMany(); + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: + ps.withFiles ? ['vmimiRelayTimelineWithFiles'] + : ps.withReplies ? ['vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies'] + : ['vmimiRelayTimeline'], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + withReplies: ps.withReplies, + }, me), + }); process.nextTick(() => { if (me) { @@ -112,7 +138,51 @@ export default class extends Endpoint { // eslint- } }); - return await this.noteEntityService.packMany(timeline, me); + return timeline; }); } + + private async getFromDb(ps: { + sinceId: string | null, + untilId: string | null, + limit: number, + withFiles: boolean, + withRenotes: boolean, + withReplies: boolean, + }, me: MiLocalUser | null) { + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + this.vmimiRelayTimelineService.generateFilterQuery(query, !ps.withReplies); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (!ps.withRenotes) { + query.andWhere(new Brackets(qb => { + qb.where('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.where('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + //#endregion + + return await query.limit(ps.limit).getMany(); + } } diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index 6ba8558dbaac..6ccb9d21bf97 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -37,7 +37,7 @@ class VmimiRelayTimelineChannel extends Channel { @bindThis public async init(params: any) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); - if (!policies.gtlAvailable) return; + if (!policies.vrtlAvailable) return; this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5003e25a2c81..df8142e46c2f 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -82,7 +82,7 @@ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>( const withReplies = computed({ get: () => { if (!$i) return false; - if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') { + if (['local', 'social', 'vmimi-relay'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') { return false; } else { return defaultStore.reactiveState.tl.value.filter.withReplies; @@ -92,7 +92,7 @@ const withReplies = computed({ }); const onlyFiles = computed({ get: () => { - if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') { + if (['local', 'social', 'vmimi-relay'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') { return false; } else { return defaultStore.reactiveState.tl.value.filter.onlyFiles; @@ -263,7 +263,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, + disabled: src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' ? withReplies : false, }], ev.currentTarget ?? ev.target); }, }, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index d8c12453eb19..1f2d7f94d62b 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4659,6 +4659,7 @@ export type components = { usersCount: number; }); RolePolicies: { + vrtlAvailable: boolean; gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; @@ -8916,6 +8917,7 @@ export type operations = { manifestJsonOverride?: string; enableFanoutTimeline?: boolean; enableFanoutTimelineDbFallback?: boolean; + vmimiRelayTimelineCacheMax?: number; perLocalUserUserTimelineCacheMax?: number; perRemoteUserUserTimelineCacheMax?: number; perUserHomeTimelineCacheMax?: number; @@ -20800,6 +20802,8 @@ export type operations = { withReplies?: boolean; /** @default 10 */ limit?: number; + /** @default true */ + allowPartial?: boolean; /** Format: misskey:id */ sinceId?: string; /** Format: misskey:id */ From 59211d92489c8dfe5d655a8cde11b64544ad3d7b Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 16 Apr 2024 16:30:37 +0900 Subject: [PATCH 13/49] =?UTF-8?q?chore(vmimi-relay):=20VRTL=E3=81=AEwithRe?= =?UTF-8?q?plies=E3=81=AE=E4=BB=95=E6=A7=98=E3=82=92LTL=E3=81=AB=E6=8F=83?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/VmimiRelayTimelineService.ts | 19 ------------------ .../endpoints/notes/vmimi-relay-timeline.ts | 20 ++++++++++++++++++- .../stream/channels/vmimi-relay-timeline.ts | 10 +++++++--- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/core/VmimiRelayTimelineService.ts b/packages/backend/src/core/VmimiRelayTimelineService.ts index 64604a19eeaa..6dd9c5efcaa6 100644 --- a/packages/backend/src/core/VmimiRelayTimelineService.ts +++ b/packages/backend/src/core/VmimiRelayTimelineService.ts @@ -74,23 +74,4 @@ export class VmimiRelayTimelineService { this.checkForUpdateInstanceList(); return this.instanceHostsArray; } - - @bindThis - generateFilterQuery(query: SelectQueryBuilder, excludeReplies: boolean) { - const names = this.hostNames; - query.andWhere(new Brackets(qb => { - qb.where('note.userHost IS NULL'); - if (names.length !== 0) { - qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names }); - } - })); - if (excludeReplies) { - query.andWhere(new Brackets(qb => { - qb.where('note.replyUserHost IS NULL'); - if (names.length !== 0) { - qb.orWhere('note.replyUserHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names }); - } - })); - } - } } diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index fa50bae677e1..032eaf06f714 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -160,7 +160,25 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.vmimiRelayTimelineService.generateFilterQuery(query, !ps.withReplies); + const vmimiRelayInstances = this.vmimiRelayTimelineService.hostNames; + query.andWhere(new Brackets(qb => { + qb.where('note.userHost IS NULL'); + if (vmimiRelayInstances.length !== 0) { + qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances }); + } + })); + + if (!ps.withReplies) { + query.andWhere(new Brackets(qb => { + qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { + qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } if (me) { this.queryService.generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index 6ccb9d21bf97..38fd9bd7b878 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -56,11 +56,15 @@ class VmimiRelayTimelineChannel extends Channel { if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 関係ない返信は除外 + if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; + } + // Ignore notes from non-vmimi relay if (!this.vmimiRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; - if (!this.withReplies && note.reply) { - if (!this.vmimiRelayTimelineService.isRelayedInstance(note.reply.user.host ?? null)) return; - } // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; From 835f75247405e5fec7dde704aa4fbd07f7a4a30e Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 16 Apr 2024 17:24:18 +0900 Subject: [PATCH 14/49] feat(vmimi-relay): vmimi relay hybrid timeline --- packages/backend/src/server/ServerModule.ts | 2 + .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../notes/vmimi-relay-hybrid-timeline.ts | 248 ++++++++++++++++++ .../src/server/api/stream/ChannelsService.ts | 3 + .../channels/vmimi-relay-hybrid-timeline.ts | 143 ++++++++++ packages/misskey-js/etc/misskey-js.api.md | 19 ++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 + packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 77 ++++++ packages/misskey-js/src/streaming.types.ts | 11 + 12 files changed, 525 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts create mode 100644 packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 711b28e50746..8f0336a84e44 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -36,6 +36,7 @@ import { ChannelChannelService } from './api/stream/channels/channel.js'; import { DriveChannelService } from './api/stream/channels/drive.js'; import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js'; import { VmimiRelayTimelineChannelService } from './api/stream/channels/vmimi-relay-timeline.js'; +import { VmimiRelayHybridTimelineChannelService } from './api/stream/channels/vmimi-relay-hybrid-timeline.js'; 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'; @@ -80,6 +81,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js DriveChannelService, GlobalTimelineChannelService, VmimiRelayTimelineChannelService, + VmimiRelayHybridTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, ReversiChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 827c9e66b7ce..ae7c4089d415 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -273,6 +273,7 @@ import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete 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_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-timeline.js'; +import * as ep___notes_vmimiRelayHybridTimeline from './endpoints/notes/vmimi-relay-hybrid-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_mentions from './endpoints/notes/mentions.js'; @@ -646,6 +647,7 @@ const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete' const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_vmimiRelayTimeline: Provider = { provide: 'ep:notes/vmimi-relay-timeline', useClass: ep___notes_vmimiRelayTimeline.default }; +const $notes_vmimiRelayHybridTimeline: Provider = { provide: 'ep:notes/vmimi-relay-hybrid-timeline', useClass: ep___notes_vmimiRelayHybridTimeline.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_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; @@ -1023,6 +1025,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_featured, $notes_globalTimeline, $notes_vmimiRelayTimeline, + $notes_vmimiRelayHybridTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, @@ -1394,6 +1397,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_featured, $notes_globalTimeline, $notes_vmimiRelayTimeline, + $notes_vmimiRelayHybridTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 28e7ef58c25f..ea466ba20a3b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -273,6 +273,7 @@ import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete 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_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-timeline.js'; +import * as ep___notes_vmimiRelayHybridTimeline from './endpoints/notes/vmimi-relay-hybrid-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_mentions from './endpoints/notes/mentions.js'; @@ -644,6 +645,7 @@ const eps = [ ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/vmimi-relay-timeline', ep___notes_vmimiRelayTimeline], + ['notes/vmimi-relay-hybrid-timeline', ep___notes_vmimiRelayHybridTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], ['notes/mentions', ep___notes_mentions], diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts new file mode 100644 index 000000000000..027e16a3470f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts @@ -0,0 +1,248 @@ +/* + * SPDX-FileCopyrightText: anatawa12 + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import type { ChannelFollowingsRepository, NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { MiLocalUser } from '@/models/User.js'; +import { MetaService } from '@/core/MetaService.js'; +import { IdService } from '@/core/IdService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:account', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + + errors: { + vmimiRelaySocialDisabled: { + message: 'Vmimi Relay Hybrid timeline has been disabled.', + code: 'VMIMI_RELAY_DISABLED', + id: 'e7496627-8086-4294-b488-63323eb80145', + }, + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: '8222638e-a5a9-495d-ae72-e825793e0a63', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + withFiles: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private roleService: RoleService, + private activeUsersChart: ActiveUsersChart, + private idService: IdService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, + private userFollowingService: UserFollowingService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + + const policies = await this.roleService.getUserPolicies(me.id); + if (!policies.vrtlAvailable) { + throw new ApiError(meta.errors.vmimiRelaySocialDisabled); + } + + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + + const serverSettings = await this.metaService.fetch(); + + if (!serverSettings.enableFanoutTimeline) { + const timeline = await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + }, me); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); + } + + let timelineConfig: FanoutTimelineName[]; + + if (ps.withFiles) { + timelineConfig = [ + `homeTimelineWithFiles:${me.id}`, + 'vmimiRelayTimelineWithFiles', + ]; + } else if (ps.withReplies) { + timelineConfig = [ + `homeTimeline:${me.id}`, + 'vmimiRelayTimeline', + 'vmimiRelayTimelineWithReplies', + ]; + } else { + timelineConfig = [ + `homeTimeline:${me.id}`, + 'vmimiRelayTimeline', + ]; + } + + const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + redisTimelines: timelineConfig, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + }, me), + }); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return redisTimeline; + }); + } + + private async getFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + withFiles: boolean, + withReplies: boolean, + }, me: MiLocalUser) { + const followees = await this.userFollowingService.getFollowees(me.id); + const followingChannels = await this.channelFollowingsRepository.find({ + where: { + followerId: me.id, + }, + }); + const vmimiRelayInstances = this.vmimiRelayTimelineService.hostNames; + + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { + if (followees.length > 0) { + const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; + qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + qb.orWhere(new Brackets(qb => { + qb.where('note.visibility = \'public\''); + qb.andWhere(new Brackets(qb => { + qb.where('note.userHost IS NULL'); + if (vmimiRelayInstances.length !== 0) { + qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances }); + } + })); + })); + } else { + qb.where('note.userId = :meId', { meId: me.id }); + qb.orWhere(new Brackets(qb => { + qb.where('note.visibility = \'public\''); + qb.andWhere(new Brackets(qb => { + qb.where('note.userHost IS NULL'); + if (vmimiRelayInstances.length !== 0) { + qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances }); + } + })); + })); + } + })) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (followingChannels.length > 0) { + const followingChannelIds = followingChannels.map(x => x.followeeId); + + query.andWhere(new Brackets(qb => { + qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); + qb.orWhere('note.channelId IS NULL'); + })); + } else { + query.andWhere('note.channelId IS NULL'); + } + + if (!ps.withReplies) { + query.andWhere(new Brackets(qb => { + qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { + qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })); + })); + } + + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + return await query.limit(ps.limit).getMany(); + } +} diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 4223a296767a..39008a0cd9a5 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -10,6 +10,7 @@ import { LocalTimelineChannelService } from './channels/local-timeline.js'; import { HomeTimelineChannelService } from './channels/home-timeline.js'; import { GlobalTimelineChannelService } from './channels/global-timeline.js'; import { VmimiRelayTimelineChannelService } from './channels/vmimi-relay-timeline.js'; +import { VmimiRelayHybridTimelineChannelService } from './channels/vmimi-relay-hybrid-timeline.js'; import { MainChannelService } from './channels/main.js'; import { ChannelChannelService } from './channels/channel.js'; import { AdminChannelService } from './channels/admin.js'; @@ -33,6 +34,7 @@ export class ChannelsService { private hybridTimelineChannelService: HybridTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, private vmimiRelayTimelineChannelService: VmimiRelayTimelineChannelService, + private vmimiRelayHybridTimelineChannelService: VmimiRelayHybridTimelineChannelService, private userListChannelService: UserListChannelService, private hashtagChannelService: HashtagChannelService, private roleTimelineChannelService: RoleTimelineChannelService, @@ -56,6 +58,7 @@ export class ChannelsService { case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; case 'vmimiRelayTimeline': return this.vmimiRelayTimelineChannelService; + case 'vmimiRelayHybridTimeline': return this.vmimiRelayHybridTimelineChannelService; case 'userList': return this.userListChannelService; case 'hashtag': return this.hashtagChannelService; case 'roleTimeline': return this.roleTimelineChannelService; diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts new file mode 100644 index 000000000000..95cc3235fe84 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.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 { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class VmimiRelayHybridTimelineChannel extends Channel { + public readonly chName = 'vmimiRelayHybridTimeline'; + public static shouldShare = false; + public static requireCredential = true as const; + public static kind = 'read:account'; + private withRenotes: boolean; + private withReplies: boolean; + private withFiles: boolean; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + //this.onNote = this.onNote.bind(this); + } + + @bindThis + public async init(params: any): Promise { + const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); + if (!policies.vrtlAvailable) return; + + this.withRenotes = params.withRenotes ?? true; + this.withReplies = params.withReplies ?? false; + this.withFiles = params.withFiles ?? false; + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @bindThis + private async onNote(note: Packed<'Note'>) { + const isMe = this.user!.id === note.userId; + + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + + // チャンネルの投稿ではなく、自分自身の投稿 または + // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または + // チャンネルの投稿ではなく、全体公開のぶいみみリレーまたはローカルの投稿 または + // フォローしているチャンネルの投稿 の場合だけ + if (!( + (note.channelId == null && isMe) || + (note.channelId == null && Object.hasOwn(this.following, note.userId)) || + (note.channelId == null && (this.vmimiRelayTimelineService.isRelayedInstance(note.user.host) && note.visibility === 'public')) || + (note.channelId != null && this.followingChannels.has(note.channelId)) + )) return; + + if (note.visibility === 'followers') { + if (!isMe && !Object.hasOwn(this.following, note.userId)) return; + } else if (note.visibility === 'specified') { + if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; + } + + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances))) return; + + if (note.reply) { + const reply = note.reply; + if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { + // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + } else { + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; + } + } + + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) 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; + + if (this.user && note.renoteId && !note.text) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + console.log(note.renote.reactionAndUserPairCache); + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } + } + + this.connection.cacheNote(note); + + this.send('note', note); + } + + @bindThis + public dispose(): void { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} + +@Injectable() +export class VmimiRelayHybridTimelineChannelService implements MiChannelService { + public readonly shouldShare = VmimiRelayHybridTimelineChannel.shouldShare; + public readonly requireCredential = VmimiRelayHybridTimelineChannel.requireCredential; + public readonly kind = VmimiRelayHybridTimelineChannel.kind; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + private vmimiRelayTimelineService: VmimiRelayTimelineService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): VmimiRelayHybridTimelineChannel { + return new VmimiRelayHybridTimelineChannel( + this.metaService, + this.roleService, + this.noteEntityService, + this.vmimiRelayTimelineService, + id, + connection, + ); + } +} diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8b1e8ddfc94f..13039d9ed63e 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -603,6 +603,17 @@ export type Channels = { }; receives: null; }; + vmimiRelayHybridTimeline: { + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; userList: { params: { listId: string; @@ -1531,6 +1542,8 @@ declare namespace entities { NotesGlobalTimelineResponse, NotesVmimiRelayTimelineRequest, NotesVmimiRelayTimelineResponse, + NotesVmimiRelayHybridTimelineRequest, + NotesVmimiRelayHybridTimelineResponse, NotesHybridTimelineRequest, NotesHybridTimelineResponse, NotesLocalTimelineRequest, @@ -2577,6 +2590,12 @@ type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requ // @public (undocumented) type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesVmimiRelayHybridTimelineRequest = operations['notes/vmimi-relay-hybrid-timeline']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesVmimiRelayHybridTimelineResponse = operations['notes/vmimi-relay-hybrid-timeline']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesVmimiRelayTimelineRequest = operations['notes/vmimi-relay-timeline']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 7a58b3eb0401..4095cc3974b8 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -2975,6 +2975,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 675511cf47f3..b4428e9919fe 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -397,6 +397,8 @@ import type { NotesGlobalTimelineResponse, NotesVmimiRelayTimelineRequest, NotesVmimiRelayTimelineResponse, + NotesVmimiRelayHybridTimelineRequest, + NotesVmimiRelayHybridTimelineResponse, NotesHybridTimelineRequest, NotesHybridTimelineResponse, NotesLocalTimelineRequest, @@ -823,6 +825,7 @@ export type Endpoints = { 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; 'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse }; 'notes/vmimi-relay-timeline': { req: NotesVmimiRelayTimelineRequest; res: NotesVmimiRelayTimelineResponse }; + 'notes/vmimi-relay-hybrid-timeline': { req: NotesVmimiRelayHybridTimelineRequest; res: NotesVmimiRelayHybridTimelineResponse }; 'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse }; 'notes/local-timeline': { req: NotesLocalTimelineRequest; res: NotesLocalTimelineResponse }; 'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 57ad9ce4bc02..f8f87041419a 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -399,6 +399,8 @@ export type NotesGlobalTimelineRequest = operations['notes/global-timeline']['re export type NotesGlobalTimelineResponse = operations['notes/global-timeline']['responses']['200']['content']['application/json']; export type NotesVmimiRelayTimelineRequest = operations['notes/vmimi-relay-timeline']['requestBody']['content']['application/json']; export type NotesVmimiRelayTimelineResponse = operations['notes/vmimi-relay-timeline']['responses']['200']['content']['application/json']; +export type NotesVmimiRelayHybridTimelineRequest = operations['notes/vmimi-relay-hybrid-timeline']['requestBody']['content']['application/json']; +export type NotesVmimiRelayHybridTimelineResponse = operations['notes/vmimi-relay-hybrid-timeline']['responses']['200']['content']['application/json']; export type NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; export type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['content']['application/json']; export type NotesLocalTimelineRequest = operations['notes/local-timeline']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 1f2d7f94d62b..1bee250f3258 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2583,6 +2583,15 @@ export type paths = { */ post: operations['notes/vmimi-relay-timeline']; }; + '/notes/vmimi-relay-hybrid-timeline': { + /** + * notes/vmimi-relay-hybrid-timeline + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['notes/vmimi-relay-hybrid-timeline']; + }; '/notes/hybrid-timeline': { /** * notes/hybrid-timeline @@ -20852,6 +20861,74 @@ export type operations = { }; }; }; + /** + * notes/vmimi-relay-hybrid-timeline + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'notes/vmimi-relay-hybrid-timeline': { + requestBody: { + content: { + 'application/json': { + /** @default false */ + withFiles?: boolean; + /** @default true */ + withRenotes?: boolean; + /** @default false */ + withReplies?: boolean; + /** @default 10 */ + limit?: number; + /** @default true */ + allowPartial?: boolean; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/hybrid-timeline * @description No description provided. diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 5374d8e1a5a7..50f6d46a2cad 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -113,6 +113,17 @@ export type Channels = { }; receives: null; }; + vmimiRelayHybridTimeline: { + params: { + withRenotes?: boolean; + withReplies?: boolean; + withFiles?: boolean; + }; + events: { + note: (payload: Note) => void; + }; + receives: null; + }; userList: { params: { listId: string; From 390b53d33b350564cfdac9bd4566cc63a2b4a57d Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 16 Apr 2024 17:47:17 +0900 Subject: [PATCH 15/49] feat(vmimi-relay/frontend): virtual kemomimi relay social timeline --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + .../frontend/src/components/MkTimeline.vue | 15 ++++++++++++- packages/frontend/src/pages/timeline.vue | 22 ++++++++++++------- packages/frontend/src/store.ts | 2 +- packages/frontend/src/ui/deck/deck-store.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 7 ++++-- 7 files changed, 40 insertions(+), 13 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index a51d35437a66..a164ff919a9d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8573,6 +8573,10 @@ export interface Locale extends ILocale { * ぶいみみリレー */ "vmimiRelay": string; + /** + * ぶいみみリレーソーシャル + */ + "vmimiRelaySocial": string; }; "_play": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 61f2db9f831e..874f368564f6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2263,6 +2263,7 @@ _timelines: social: "ソーシャル" global: "グローバル" vmimiRelay: "ぶいみみリレー" + vmimiRelaySocial: "ぶいみみリレーソーシャル" _play: new: "Playの作成" diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index a1c1ea234118..f8e7c41f77bf 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -29,7 +29,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'vmimi-relay-social' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; list?: string; antenna?: string; channel?: string; @@ -127,6 +127,12 @@ function connectChannel() { withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, }); + } else if (props.src === 'vmimi-relay-social') { + connection = stream.useChannel('vmimiRelayHybridTimeline', { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + withReplies: props.withReplies, + }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); connection.on('mention', prepend); @@ -206,6 +212,13 @@ function updatePaginationQuery() { withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, }; + } else if (props.src === 'vmimi-relay-social') { + endpoint = 'notes/vmimi-relay-hybrid-timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + withReplies: props.withReplies, + }; } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; query = null; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index df8142e46c2f..7d4911f4833c 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -57,6 +57,7 @@ import { miLocalStorage } from '@/local-storage.js'; provide('shouldOmitHeaderTitle', true); const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); +const isVmimiRelayTimelineAvailable = ($i == null && instance.policies.vrtlAvailable) || ($i != null && $i.policies.vrtlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); const keymap = { 't': focus, @@ -67,7 +68,7 @@ const rootEl = shallowRef(); const queue = ref(0); const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global'); -const src = computed<'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`>({ +const src = computed<'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'vmimi-relay-social' | `list:${string}`>({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), }); @@ -82,7 +83,7 @@ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>( const withReplies = computed({ get: () => { if (!$i) return false; - if (['local', 'social', 'vmimi-relay'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') { + if (['local', 'social', 'vmimi-relay', 'vmimi-relay-social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') { return false; } else { return defaultStore.reactiveState.tl.value.filter.withReplies; @@ -92,7 +93,7 @@ const withReplies = computed({ }); const onlyFiles = computed({ get: () => { - if (['local', 'social', 'vmimi-relay'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') { + if (['local', 'social', 'vmimi-relay', 'vmimi-relay-social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') { return false; } else { return defaultStore.reactiveState.tl.value.filter.onlyFiles; @@ -199,7 +200,7 @@ async function chooseChannel(ev: MouseEvent): Promise { os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`): void { +function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'vmimi-relay-social' | `list:${string}`): void { const out = deepMerge({ src: newSrc }, defaultStore.state.tl); if (newSrc.startsWith('userList:')) { @@ -250,7 +251,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' ? { + }, src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -263,7 +264,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' ? withReplies : false, + disabled: src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? withReplies : false, }], ev.currentTarget ?? ev.target); }, }, @@ -305,11 +306,16 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList title: i18n.ts._timelines.global, icon: 'ti ti-whirl', iconOnly: true, -}, { +}] : []), ...(isVmimiRelayTimelineAvailable ? [{ key: 'vmimi-relay', title: i18n.ts._timelines.vmimiRelay, icon: 'ti ti-circles-relation', iconOnly: true, +}, { + key: 'vmimi-relay-social', + title: i18n.ts._timelines.vmimiRelaySocial, + icon: 'ti ti-topology-full', + iconOnly: true, }] : []), { icon: 'ti ti-list', title: i18n.ts.lists, @@ -344,7 +350,7 @@ const headerTabsWhenNotLogin = computed(() => [ definePageMetadata(() => ({ title: i18n.ts.timeline, - icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : src.value === 'vmimi-relay' ? 'ti ti-circles-relation' : 'ti ti-home', + icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : src.value === 'vmimi-relay' ? 'ti ti-circles-relation' : src.value === 'vmimi-relay-social' ? 'ti ti-topology-full' : 'ti ti-home', })); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 516fc296a18a..bb8c91e3372b 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -184,7 +184,7 @@ export const defaultStore = markRaw(new Storage('base', { tl: { where: 'deviceAccount', default: { - src: 'home' as 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | `list:${string}`, + src: 'home' as 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'vmimi-relay-social' | `list:${string}`, userList: null as Misskey.entities.UserList | null, filter: { withReplies: true, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index a41b5ecb28c5..bb61e754feac 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -29,7 +29,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay'; + tl?: 'home' | 'local' | 'social' | 'global' | 'vmimi-relay' | 'vmimi-relay-social'; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 0090f4fd694f..a930a8c2dcef 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ column.name }} @@ -98,6 +99,8 @@ async function setType() { value: 'global' as const, text: i18n.ts._timelines.global, }, { value: 'vmimi-relay' as const, text: i18n.ts._timelines.vmimiRelay, + }, { + value: 'vmimi-relay-social' as const, text: i18n.ts._timelines.vmimiRelaySocial, }], }); if (canceled) { @@ -119,7 +122,7 @@ const menu = [{ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, -}, props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay' ? { +}, props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -128,7 +131,7 @@ const menu = [{ type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false, + disabled: props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? withReplies : false, }]; From bfa5f42c3ec23a9dbf763f8b93db0ff229a2c7b4 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Wed, 17 Apr 2024 12:12:49 +0900 Subject: [PATCH 16/49] =?UTF-8?q?chore:=20=E3=81=B6=E3=81=84=E3=81=BF?= =?UTF-8?q?=E3=81=BF=E3=83=AA=E3=83=AC=E3=83=BC=E3=82=BD=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A3=E3=83=AB=20>=20=E3=81=B6=E3=81=84=E3=81=BF=E3=81=BF?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B7=E3=83=A3=E3=83=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 2 +- locales/ja-JP.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index a164ff919a9d..b4cd525a346f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8574,7 +8574,7 @@ export interface Locale extends ILocale { */ "vmimiRelay": string; /** - * ぶいみみリレーソーシャル + * ぶいみみソーシャル */ "vmimiRelaySocial": string; }; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 874f368564f6..c0231da27eeb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2263,7 +2263,7 @@ _timelines: social: "ソーシャル" global: "グローバル" vmimiRelay: "ぶいみみリレー" - vmimiRelaySocial: "ぶいみみリレーソーシャル" + vmimiRelaySocial: "ぶいみみソーシャル" _play: new: "Playの作成" From efe9cc62daedefbcdd4949171c0e73f7c4e1adc2 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Wed, 17 Apr 2024 12:13:13 +0900 Subject: [PATCH 17/49] =?UTF-8?q?feat:=20=E3=81=B6=E3=81=84=E3=81=BF?= =?UTF-8?q?=E3=81=BF=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=81=AB=E8=AA=AC=E6=98=8E=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 8 ++++++++ locales/ja-JP.yml | 2 ++ packages/frontend/src/pages/timeline.vue | 4 ++-- packages/frontend/src/store.ts | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index b4cd525a346f..7ce812751fdd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5315,6 +5315,14 @@ export interface Locale extends ILocale { * グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。 */ "global": string; + /** + * ぶいみみリレータイムラインでは、バーチャルケモミミリレーサーバーに参加しているサーバーのユーザー全員の投稿を見られます。 + */ + "vmimi-relay": string; + /** + * ぶいみみソーシャルタイムラインには、ホームタイムラインとぶいみみリレータイムラインの投稿が両方表示されます。 + */ + "vmimi-relay-social": string; }; "_serverRules": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c0231da27eeb..82bd5406578b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1339,6 +1339,8 @@ _timelineDescription: local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。" social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。" + vmimi-relay: "ぶいみみリレータイムラインでは、バーチャルケモミミリレーサーバーに参加しているサーバーのユーザー全員の投稿を見られます。" + vmimi-relay-social: "ぶいみみソーシャルタイムラインには、ホームタイムラインとぶいみみリレータイムラインの投稿が両方表示されます。" _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 7d4911f4833c..af3fb03b0d04 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ i18n.ts._timelineDescription[src] }} @@ -235,7 +235,7 @@ function focus(): void { } function closeTutorial(): void { - if (!['home', 'local', 'social', 'global'].includes(src.value)) return; + if (!['home', 'local', 'social', 'global', 'vmimi-relay', 'vmimi-relay-social'].includes(src.value)) return; const before = defaultStore.state.timelineTutorials; before[src.value] = true; defaultStore.set('timelineTutorials', before); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index bb8c91e3372b..4d1922dc49be 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -75,6 +75,8 @@ export const defaultStore = markRaw(new Storage('base', { local: false, social: false, global: false, + 'vmimi-relay': false, + 'vmimi-relay-social': false, }, }, keepCw: { From 3c1d39ff309669206aba130ff41eb58595989757 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Wed, 17 Apr 2024 22:36:29 +0900 Subject: [PATCH 18/49] =?UTF-8?q?fix:=20vrtlAvailable=E3=82=92web=E3=81=8B?= =?UTF-8?q?=E3=82=89=E3=81=84=E3=81=98=E3=82=8C=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + .../src/core/entities/MetaEntityService.ts | 1 + .../backend/src/models/json-schema/meta.ts | 4 ++++ packages/frontend/src/const.ts | 1 + .../frontend/src/pages/admin/roles.editor.vue | 20 +++++++++++++++++++ packages/frontend/src/pages/admin/roles.vue | 8 ++++++++ packages/misskey-js/src/autogen/types.ts | 1 + 8 files changed, 40 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index 7ce812751fdd..1ea07e11396f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6446,6 +6446,10 @@ export interface Locale extends ILocale { * ローカルタイムラインの閲覧 */ "ltlAvailable": string; + /** + * ぶいみみリレータイムラインの閲覧 + */ + "vrtlAvailable": string; /** * パブリック投稿の許可 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 82bd5406578b..500f29faf2bb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1666,6 +1666,7 @@ _role: _options: gtlAvailable: "グローバルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" + vrtlAvailable: "ぶいみみリレータイムラインの閲覧" canPublicNote: "パブリック投稿の許可" mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index b50d76288f27..8df7ae0cafb8 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -137,6 +137,7 @@ export class MetaEntityService { features: { localTimeline: instance.policies.ltlAvailable, globalTimeline: instance.policies.gtlAvailable, + vrtlAvailable: instance.policies.vrtlAvailable, registration: !instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, hcaptcha: instance.enableHcaptcha, diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 17789f3b46ce..34ba9ad24568 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -266,6 +266,10 @@ export const packedMetaDetailedOnlySchema = { type: 'boolean', optional: false, nullable: false, }, + vrtlAvailable: { + type: 'boolean', + optional: false, nullable: false, + }, hcaptcha: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 9e41926a97a1..fa6fd253a851 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -74,6 +74,7 @@ export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const ROLE_POLICIES = [ 'gtlAvailable', 'ltlAvailable', + 'vrtlAvailable', 'canPublicNote', 'mentionLimit', 'canInvite', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index eb8a59b34f48..cc066d445090 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -140,6 +140,26 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + +
+ + + + + + + + + +
+
+ -
+

{{ i18n.ts._disabledTimeline.title }} @@ -48,6 +50,7 @@ import { instance } from '@/instance.js'; const name = 'timeline'; const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); +const isVmimiRelayTimelineAvailable = (($i == null && instance.policies.vrtlAvailable) || ($i != null && $i.policies.vrtlAvailable)); const widgetPropsDef = { showHeader: { @@ -131,6 +134,14 @@ const choose = async (ev) => { text: i18n.ts._timelines.global, icon: 'ti ti-whirl', action: () => { setSrc('global'); }, + }, { + text: i18n.ts._timelines.vmimiRelay, + icon: 'ti ti-whirl', + action: () => { setSrc('vmimi-relay'); }, + }, { + text: i18n.ts._timelines.vmimiRelaySocial, + icon: 'ti ti-whirl', + action: () => { setSrc('vmimi-relay-social'); }, }, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); From c947c192c3f0e4966a21b56734b1ea933748b445 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 16 Jun 2024 21:21:53 +0900 Subject: [PATCH 35/49] =?UTF-8?q?CHANGELOG=E3=82=92=E6=97=A5=E6=9C=AC?= =?UTF-8?q?=E8=AA=9E=E3=81=AB=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG-VRTL.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG-VRTL.md b/CHANGELOG-VRTL.md index 03b56d6cf732..7d5bca9d614f 100644 --- a/CHANGELOG-VRTL.md +++ b/CHANGELOG-VRTL.md @@ -1,8 +1,9 @@ # CHANGELOG about VRTL -This file lists changes to VRTL Generic Branch. +VRTLのブランチで行われた変更点をまとめています -- Fix VRTL/VSTL is not avaiable on timeline widgets -- Fix replies to me are not included in V\[RS]TL if withReplies is disabled. -- Add `vmimiRelayTimelineImplemented` and `disableVmimiRelayTimeline` to nodeinfo +- fix(frontend): ウィジェットでVRTL/VSTLが使用できない問題を修正 +- fix(backend): 自分自身に対するリプライがwithReplies = falseなVRTL/VSTLにて含まれていない問題を修正 +- feat(backend): `vmimiRelayTimelineImplemented` と `disableVmimiRelayTimeline` nodeinfo に追加しました + - これによりサードパーティクライアントがVRTLの有無を認知できるようになりました。 From 24be6b1578b03674b2c7f9f11da3b18b1d8792e5 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 16 Jun 2024 22:09:28 +0900 Subject: [PATCH 36/49] =?UTF-8?q?VRTL/VSTL=E3=81=AB=E9=80=A3=E5=90=88?= =?UTF-8?q?=E3=81=AA=E3=81=97=E6=8A=95=E7=A8=BF=E3=82=92=E5=90=AB=E3=82=81?= =?UTF-8?q?=E3=82=8B=E3=81=8B=E3=82=92=E9=81=B8=E6=8A=9E=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=81=AB=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): withLocalOnly to V[RS]TL * feat(frontend): TLに連合なし投稿を含めるかを選択可能に * docs(changelog): feat: VRTL/VSTLに連合なし投稿を含めるかを選択可能に --- CHANGELOG-VRTL.md | 3 +++ locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/backend/src/core/NoteCreateService.ts | 4 ++-- .../notes/vmimi-relay-hybrid-timeline.ts | 9 +++++++++ .../api/endpoints/notes/vmimi-relay-timeline.ts | 16 ++++++++++++---- .../channels/vmimi-relay-hybrid-timeline.ts | 4 +++- .../api/stream/channels/vmimi-relay-timeline.ts | 3 +++ packages/frontend/src/components/MkTimeline.vue | 7 +++++++ packages/frontend/src/pages/timeline.vue | 13 +++++++++++-- packages/frontend/src/store.ts | 1 + packages/frontend/src/ui/deck/deck-store.ts | 1 + packages/frontend/src/ui/deck/tl-column.vue | 16 ++++++++++++++-- packages/misskey-js/etc/misskey-js.api.md | 2 ++ packages/misskey-js/src/autogen/types.ts | 4 ++++ packages/misskey-js/src/streaming.types.ts | 2 ++ 16 files changed, 79 insertions(+), 11 deletions(-) diff --git a/CHANGELOG-VRTL.md b/CHANGELOG-VRTL.md index 7d5bca9d614f..7c3dc085ce6a 100644 --- a/CHANGELOG-VRTL.md +++ b/CHANGELOG-VRTL.md @@ -3,6 +3,9 @@ VRTLのブランチで行われた変更点をまとめています +- feat: VRTL/VSTLに連合なし投稿を含めるかを選択可能に + - もともとのVRTL/VSTLでは連合なし投稿が常に含まれていましたが、正しくVRTL/VSTLのノートを表現するために含めないようにできるようになりました + - VSTLの場合、連合なし投稿を含めないようにしてもフォローしている人の連合なし投稿は表示されます - fix(frontend): ウィジェットでVRTL/VSTLが使用できない問題を修正 - fix(backend): 自分自身に対するリプライがwithReplies = falseなVRTL/VSTLにて含まれていない問題を修正 - feat(backend): `vmimiRelayTimelineImplemented` と `disableVmimiRelayTimeline` nodeinfo に追加しました diff --git a/locales/index.d.ts b/locales/index.d.ts index 28ab21630a4e..71c0927dabfe 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4716,6 +4716,10 @@ export interface Locale extends ILocale { * TLに現在フォロー中の人全員の返信を含めないようにする */ "hideRepliesToOthersInTimelineAll": string; + /** + * TLに連合なし投稿を含める + */ + "showLocalOnlyInTimeline": string; /** * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか? */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 699585d6fcf6..1450a42e63b9 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1175,6 +1175,7 @@ showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする" hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする" +showLocalOnlyInTimeline: "TLに連合なし投稿を含める" confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" externalServices: "外部サービス" diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 27b66167a382..e0a9e7c1badc 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -953,7 +953,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); } } - if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) { this.fanoutTimelineService.push('vmimiRelayTimelineWithReplies', note.id, meta.vmimiRelayTimelineCacheMax, r); if (note.replyUserHost == null) { this.fanoutTimelineService.push(`vmimiRelayTimelineWithReplyTo:${note.replyUserId}`, note.id, meta.vmimiRelayTimelineCacheMax / 10, r); @@ -971,7 +971,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } - if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) { this.fanoutTimelineService.push('vmimiRelayTimeline', note.id, meta.vmimiRelayTimelineCacheMax, r); if (note.fileIds.length > 0) { this.fanoutTimelineService.push('vmimiRelayTimelineWithFiles', note.id, meta.vmimiRelayTimelineCacheMax / 2, r); diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts index 483bca4d2ecd..f798a05e0c6d 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts @@ -57,6 +57,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withLocalOnly: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default sinceId: { type: 'string', format: 'misskey:id' }, @@ -106,6 +107,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, withFiles: ps.withFiles, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me); process.nextTick(() => { @@ -122,18 +124,21 @@ export default class extends Endpoint { // eslint- `homeTimelineWithFiles:${me.id}`, 'vmimiRelayTimelineWithFiles', ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimelineWithFiles']; } else if (ps.withReplies) { timelineConfig = [ `homeTimeline:${me.id}`, 'vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies', ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimeline', 'localTimelineWithReplies']; } else { timelineConfig = [ `homeTimeline:${me.id}`, 'vmimiRelayTimeline', `vmimiRelayTimelineWithReplyTo:${me.id}`, ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimeline', `localTimelineWithReplyTo:${me.id}`]; } const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ @@ -152,6 +157,7 @@ export default class extends Endpoint { // eslint- limit, withFiles: ps.withFiles, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me), }); @@ -169,6 +175,7 @@ export default class extends Endpoint { // eslint- limit: number, withFiles: boolean, withReplies: boolean, + withLocalOnly: boolean, }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ @@ -185,6 +192,7 @@ export default class extends Endpoint { // eslint- qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); qb.orWhere(new Brackets(qb => { qb.where('note.visibility = \'public\''); + if (!ps.withLocalOnly) qb.andWhere('note.localOnly = FALSE'); qb.andWhere(new Brackets(qb => { qb.where('note.userHost IS NULL'); if (vmimiRelayInstances.length !== 0) { @@ -196,6 +204,7 @@ export default class extends Endpoint { // eslint- qb.where('note.userId = :meId', { meId: me.id }); qb.orWhere(new Brackets(qb => { qb.where('note.visibility = \'public\''); + if (!ps.withLocalOnly) qb.andWhere('note.localOnly = FALSE'); qb.andWhere(new Brackets(qb => { qb.where('note.userHost IS NULL'); if (vmimiRelayInstances.length !== 0) { diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index 4dc1e3b78100..fddf282f7a64 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -52,6 +52,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withLocalOnly: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default sinceId: { type: 'string', format: 'misskey:id' }, @@ -98,6 +99,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me); process.nextTick(() => { @@ -117,10 +119,10 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: - ps.withFiles ? ['vmimiRelayTimelineWithFiles'] - : ps.withReplies ? ['vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies'] - : me ? ['vmimiRelayTimeline', `vmimiRelayTimelineWithReplyTo:${me.id}`] - : ['vmimiRelayTimeline'], + ps.withFiles ? ['vmimiRelayTimelineWithFiles', ...(ps.withLocalOnly ? ['localTimelineWithFiles'] as const : [])] + : ps.withReplies ? ['vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies', ...(ps.withLocalOnly ? ['localTimeline', 'localTimelineWithReplies'] as const : [])] + : me ? ['vmimiRelayTimeline', `vmimiRelayTimelineWithReplyTo:${me.id}`, ...(ps.withLocalOnly ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] as const : [])] + : ['vmimiRelayTimeline', ...(ps.withLocalOnly ? ['localTimeline'] as const : [])], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -130,6 +132,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me), }); @@ -150,6 +153,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withRenotes: boolean, withReplies: boolean, + withLocalOnly: boolean, }, me: MiLocalUser | null) { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -169,6 +173,10 @@ export default class extends Endpoint { // eslint- } })); + if (!ps.withLocalOnly) { + query.andWhere('note.localOnly = FALSE'); + } + if (!ps.withReplies) { query.andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts index 229a52d411d1..eb0663f7f7d7 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts @@ -22,6 +22,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withLocalOnly: boolean; constructor( private metaService: MetaService, @@ -44,6 +45,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withLocalOnly = params.withLocalOnly ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -62,7 +64,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { if (!( (note.channelId == null && isMe) || (note.channelId == null && Object.hasOwn(this.following, note.userId)) || - (note.channelId == null && (this.vmimiRelayTimelineService.isRelayedInstance(note.user.host) && note.visibility === 'public')) || + (note.channelId == null && (this.vmimiRelayTimelineService.isRelayedInstance(note.user.host) && note.visibility === 'public') && (this.withLocalOnly || !note.localOnly)) || (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index 3fd7068c1cc6..b07261419bea 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -20,6 +20,7 @@ class VmimiRelayTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withLocalOnly: boolean; constructor( private metaService: MetaService, @@ -41,6 +42,7 @@ class VmimiRelayTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withLocalOnly = params.withLocalOnly ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -51,6 +53,7 @@ class VmimiRelayTimelineChannel extends Channel { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!this.vmimiRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; + if (!this.withLocalOnly && note.localOnly) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index f8e7c41f77bf..d0deb2dc2984 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -38,10 +38,12 @@ const props = withDefaults(defineProps<{ withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + withLocalOnly?: boolean; }>(), { withRenotes: true, withReplies: false, onlyFiles: false, + withLocalOnly: true, }); const emit = defineEmits<{ @@ -57,6 +59,7 @@ type TimelineQueryType = { withRenotes?: boolean, withReplies?: boolean, withFiles?: boolean, + withLocalOnly?: boolean, visibility?: string, listId?: string, channelId?: string, @@ -126,12 +129,14 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }); } else if (props.src === 'vmimi-relay-social') { connection = stream.useChannel('vmimiRelayHybridTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); @@ -211,6 +216,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }; } else if (props.src === 'vmimi-relay-social') { endpoint = 'notes/vmimi-relay-hybrid-timeline'; @@ -218,6 +224,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }; } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 49175b2b5a8a..bba107c23327 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -17,12 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only

@@ -76,6 +77,10 @@ const withRenotes = computed({ get: () => defaultStore.reactiveState.tl.value.filter.withRenotes, set: (x) => saveTlFilter('withRenotes', x), }); +const withLocalOnly = computed({ + get: () => defaultStore.reactiveState.tl.value.filter.withLocalOnly, + set: (x) => saveTlFilter('withLocalOnly', x), +}); // computed内での無限ループを防ぐためのフラグ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies'); @@ -263,7 +268,11 @@ const headerActions = computed(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? withReplies : false, - }], ev.currentTarget ?? ev.target); + }, src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? { + type: 'switch', + text: i18n.ts.showLocalOnlyInTimeline, + ref: withLocalOnly, + } : undefined], ev.currentTarget ?? ev.target); }, }, ]; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 2b401a2b94d0..cae8aa584c0d 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -192,6 +192,7 @@ export const defaultStore = markRaw(new Storage('base', { withRenotes: true, withSensitive: true, onlyFiles: false, + withLocalOnly: true, }, }, }, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 77b4d68e0839..c9dae2e6f3fb 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -34,6 +34,7 @@ export type Column = { withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + withLocalOnly?: boolean; soundSetting: SoundStore; }; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 751adc076d0a..17b8f7d88855 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -25,11 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -63,6 +64,7 @@ const soundSetting = ref(props.column.soundSetting ?? { type: null, const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); const onlyFiles = ref(props.column.onlyFiles ?? false); +const withLocalOnly = ref(props.column.withLocalOnly ?? true); watch(withRenotes, v => { updateColumn(props.column.id, { @@ -82,6 +84,12 @@ watch(onlyFiles, v => { }); }); +watch(withLocalOnly, v => { + updateColumn(props.column.id, { + withLocalOnly: v, + }); +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); @@ -150,7 +158,11 @@ const menu: MenuItem[] = [{ text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? withReplies : false, -}]; +}, props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? { + type: 'switch', + text: i18n.ts.showLocalOnlyInTimeline, + ref: withLocalOnly, +} : undefined];