diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcbdf58da..f00a78cd5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ --> -## 2023.9.0 (unreleased) +## 2023.9.0-mattyaski (unreleased) ### General - OAuth 2.0のサポート @@ -48,7 +48,10 @@ - Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように - Enhance: Renote自体を通報できるように - Enhance: データセーバーモードの強化 + - Safariは非対応だけど、LTEだと自動的にオンにする機能も追加 (https://developer.mozilla.org/ja/docs/Web/API/Navigator/connection) + - アイコンをblurで表示させるように - Enhance: Renoteを管理者権限で削除可能に +- Enhance: `$[mix ]`(emojiKitchen) 記法を追加 # mattyaski独自 - `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました - Playの操作を行うAPI TokenをAPIコンソールから発行できるように - リアクションの表示サイズをより大きくできるように @@ -70,7 +73,10 @@ - Webhookのペイロードにサーバーのurlが含まれるようになりました - Webhook設定でsecretを空に出来るように - 使われていないアンテナの自動停止を設定可能に +- リプライをホーム投稿に # mattyaski独自 - nodeinfo 2.1対応 +- Enhance: 絵文字の重複登録を不可に # mattyaski独自 +- Enhance: frontendのbuildに圧縮をかけるように # mattyaski独自 - 自分へのメンション一覧を取得する際のパフォーマンスを向上 - Docker環境でjemallocを使用することでメモリ使用量を削減 - Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正 diff --git a/locales/en-US.yml b/locales/en-US.yml index e0358a8468..772e6f35da 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1029,6 +1029,9 @@ noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" dataSaver: "Data Saver" +cellularWithDataSaver: "Turn on Data Saver in Mobile Data Communications" +UltimatedataSaver: "Ultimate Data Saver" +cellularWithUltimateDataSaver: "Turn on Ultimate Data Saver in Mobile Data Communications" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" accountMovedShort: "This account has been migrated." @@ -1551,6 +1554,7 @@ _aboutMisskey: contributors: "Main contributors" allContributors: "All contributors" source: "Source code" + forksource: "Source code for this fork" translation: "Translate Misskey" donate: "Donate to Misskey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" diff --git a/locales/index.d.ts b/locales/index.d.ts index ac714258e2..32df226912 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1032,6 +1032,9 @@ export interface Locale { "video": string; "videos": string; "dataSaver": string; + "cellularWithDataSaver": string; + "UltimateDataSaver": string; + "cellularWithUltimateDataSaver": string; "accountMigration": string; "accountMoved": string; "accountMovedShort": string; @@ -1656,6 +1659,7 @@ export interface Locale { "contributors": string; "allContributors": string; "source": string; + "forksource": string; "translation": string; "donate": string; "morePatrons": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d97b09f63c..7cad2361bf 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1029,6 +1029,9 @@ noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" dataSaver: "データセーバー" +cellularWithDataSaver: "モバイルデータ通信でデータセーバーをオンにする" +UltimateDataSaver: "究極のデータセーバー" +cellularWithUltimateDataSaver: "モバイルデータ通信で究極のデータセーバーをオンにする" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" accountMovedShort: "このアカウントは移行されています" @@ -1573,6 +1576,7 @@ _aboutMisskey: contributors: "主なコントリビューター" allContributors: "全てのコントリビューター" source: "ソースコード" + forksource: "当フォークのソースコード" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" diff --git a/package.json b/package.json index ffb4596e90..70a5727fe0 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "misskey", - "version": "2023.9.0-beta.8", + "version": "2023.9.0-beta.8-mattyaski.5", "codename": "nasubi", "repository": { "type": "git", - "url": "https://github.com/misskey-dev/misskey.git" + "url": "https://github.com/mattyatea/misskey.git" }, "packageManager": "pnpm@8.7.5", "workspaces": [ diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 11721263d3..247ef73de8 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -265,7 +265,8 @@ export class FileServerService { 'avatar' in request.query || 'static' in request.query || 'preview' in request.query || - 'badge' in request.query + 'badge' in request.query || + 'datasaver' in request.query ) { if (!isConvertibleImage) { // 画像でないなら404でお茶を濁す @@ -284,7 +285,7 @@ export class FileServerService { } else { const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) .resize({ - height: 'emoji' in request.query ? 128 : 320, + height: 'emoji' in request.query ? 64 : 128, withoutEnlargement: true, }) .webp(webpDefault); @@ -330,7 +331,28 @@ export class FileServerService { ext: 'png', type: 'image/png', }; - } else if (file.mime === 'image/svg+xml') { + } else if ('datasaver' in request.query){ + if (!isAnimationConvertibleImage && !('static' in request.query)) { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } else { + const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) + .resize({ + height: 32, + withoutEnlargement: true, + }) + .webp(webpDefault); + + image = { + data, + ext: 'webp', + type: 'image/webp', + }; + } + }else if (file.mime === 'image/svg+xml') { image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048); } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index fa81380f01..ed2c981136 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -34,6 +34,7 @@ import { GlobalTimelineChannelService } from './api/stream/channels/global-timel import { HashtagChannelService } from './api/stream/channels/hashtag.js'; import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js'; import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js'; +import { HybridAllTimelineChannelService } from './api/stream/channels/hybrid-all-timeline.js'; import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js'; import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; @@ -79,6 +80,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; RoleTimelineChannelService, HomeTimelineChannelService, HybridTimelineChannelService, + HybridAllTimelineChannelService, LocalTimelineChannelService, QueueStatsChannelService, ServerStatsChannelService, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b9fa6c3b0..1829fc16f3 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -262,6 +262,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_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; +import * as ep___notes_hybrid_All_Timeline from './endpoints/notes/hybrid-all-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; @@ -609,6 +610,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_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; +const $notes_hybridAllTimeline: Provider = { provide: 'ep:notes/hybrid-all-timeline', useClass: ep___notes_hybrid_All_Timeline.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 }; const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; @@ -960,6 +962,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_featured, $notes_globalTimeline, $notes_hybridTimeline, + $notes_hybridAllTimeline, $notes_localTimeline, $notes_mentions, $notes_polls_recommendation, @@ -1305,6 +1308,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_featured, $notes_globalTimeline, $notes_hybridTimeline, + $notes_hybridAllTimeline, $notes_localTimeline, $notes_mentions, $notes_polls_recommendation, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a9cb7c341a..86ea93139e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -262,6 +262,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_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; +import * as ep___notes_hybrid_All_Timeline from './endpoints/notes/hybrid-all-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; @@ -607,6 +608,7 @@ const eps = [ ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], + ['notes/hybrid-all-timeline', ep___notes_hybrid_All_Timeline], ['notes/local-timeline', ep___notes_localTimeline], ['notes/mentions', ep___notes_mentions], ['notes/polls/recommendation', ep___notes_polls_recommendation], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 7bd920c312..4ae1b8aa11 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, EmojisRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -24,6 +24,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, + duplicationEmojiAdd: { + message: 'This emoji is already added.', + code: 'DUPLICATION_EMOJI_ADD', + id: 'mattyaski_emoji_duplication_error', + } }, } as const; @@ -57,7 +62,8 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, private customEmojiService: CustomEmojiService, private emojiEntityService: EmojiEntityService, @@ -67,6 +73,20 @@ export default class extends Endpoint { // eslint- const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + const duplicationEmoji = await this.emojisRepository.find({ + where: { + name: ps.name, + }, + }); + + duplicationEmoji.forEach( + (emoji) => { + if (emoji.name === ps.name) { + throw new ApiError(meta.errors.duplicationEmojiAdd); + } + } + ) + const emoji = await this.customEmojiService.add({ driveFile, name: ps.name, @@ -79,6 +99,8 @@ export default class extends Endpoint { // eslint- roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], }); + + this.moderationLogService.insertModerationLog(me, 'addEmoji', { emojiId: emoji.id, }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index f374b31303..83b093e931 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -13,6 +13,7 @@ import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; +import {IsNull} from "typeorm"; export const meta = { tags: ['admin'], @@ -26,6 +27,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: 'e2785b66-dca3-4087-9cac-b93c541cc425', }, + duplicationEmojiAdd: { + message: 'This emoji is already added.', + code: 'DUPLICATION_EMOJI_ADD', + id: 'mattyaski_emoji_duplication_error', + } }, res: { @@ -57,6 +63,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + private emojiEntityService: EmojiEntityService, private idService: IdService, private globalEventService: GlobalEventService, @@ -69,6 +76,21 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchEmoji); } + const duplicationEmoji = await this.emojisRepository.find({ + where: { + name: emoji.name, + host: IsNull() + }, + }); + + duplicationEmoji.forEach( + (_emoji) => { + if (_emoji.name === emoji.name) { + throw new ApiError(meta.errors.duplicationEmojiAdd); + } + } + ) + let driveFile: MiDriveFile; try { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index f01be9e27a..dfe5266382 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository , EmojisRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -32,6 +32,11 @@ export const meta = { code: 'SAME_NAME_EMOJI_EXISTS', id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', }, + duplicationEmojiAdd: { + message: 'This emoji is already added.', + code: 'DUPLICATION_EMOJI_ADD', + id: 'mattyaski_emoji_duplication_error', + } }, } as const; @@ -64,7 +69,6 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index c7e7ca30c2..aa963ec3e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import {noteVisibilities} from "@/types.js"; export const meta = { tags: ['notes'], @@ -232,7 +233,7 @@ export default class extends Endpoint { // eslint- } } } - + let visibility = ps.visibility; let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply @@ -243,7 +244,10 @@ export default class extends Endpoint { // eslint- } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } - + // ノートがリプライでパブリック投稿の場合はホームにする + if (ps.visibility != 'home' && ps.visibility!== 'followers' && ps.visibility!=='specified' ){ + visibility = 'home'; + } // Check blocking if (reply.userId !== me.id) { const blockExist = await this.blockingsRepository.exist({ @@ -292,7 +296,7 @@ export default class extends Endpoint { // eslint- cw: ps.cw, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, - visibility: ps.visibility, + visibility, visibleUsers, channel, apMentions: ps.noExtractMentions ? [] : undefined, diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-all-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-all-timeline.ts new file mode 100644 index 0000000000..d3d9fa346a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-all-timeline.ts @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Brackets } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + + errors: { + ltlDisabled: { + message: 'hybrid Local timeline has been disabled.', + code: 'LTL_DISABLED', + id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, + fileType: { type: 'array', items: { + type: 'string', + } }, + excludeNsfw: { type: 'boolean', default: false }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@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, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me ? me.id : null); + if (!policies.ltlAvailable) { + throw new ApiError(meta.errors.ltlDisabled); + } + + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + .andWhere('(note.visibility = \'home\') AND (note.userHost IS NULL)') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + this.queryService.generateChannelQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateMutedNoteQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); + + if (ps.excludeNsfw) { + query.andWhere('note.cw IS NULL'); + query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + } + } + //#endregion + + const timeline = await query.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/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 5bb8196543..d5f3c420d6 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -48,7 +48,6 @@ export default class extends Endpoint { // eslint- if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - return await this.noteEntityService.pack(note, me, { detail: true, }); diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 8fd106c10c..c418d314a6 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -19,7 +19,7 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; - +import { HybridAllTimelineChannelService } from './channels/hybrid-all-timeline.js'; @Injectable() export class ChannelsService { constructor( @@ -27,6 +27,7 @@ export class ChannelsService { private homeTimelineChannelService: HomeTimelineChannelService, private localTimelineChannelService: LocalTimelineChannelService, private hybridTimelineChannelService: HybridTimelineChannelService, + private hybridAllTimelineChannelService: HybridAllTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, private userListChannelService: UserListChannelService, private hashtagChannelService: HashtagChannelService, @@ -47,6 +48,7 @@ export class ChannelsService { case 'homeTimeline': return this.homeTimelineChannelService; case 'localTimeline': return this.localTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; + case 'hybridAllTimeline': return this.hybridAllTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; case 'userList': return this.userListChannelService; case 'hashtag': return this.hashtagChannelService; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts new file mode 100644 index 0000000000..bca26eb36a --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/hybrid-all-timeline.ts @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import Channel from '../channel.js'; + +class HybridAllTimelineChannel extends Channel { + public readonly chName = 'hybridAllTimeline'; + public static shouldShare = true; + public static requireCredential = false; + private withReplies: boolean; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + //this.onNote = this.onNote.bind(this); + } + + @bindThis + public async init(params: any) { + const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); + if (!policies.ltlAvailable) return; + + this.withReplies = params.withReplies as boolean; + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @bindThis + private async onNote(note: Packed<'Note'>) { + if (note.user.host !== null) return; + if (note.visibility === "public") return; + if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; + + // リプライなら再pack + if (note.replyId != null) { + note.reply = await this.noteEntityService.pack(note.replyId, this.user, { + detail: true, + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { + detail: true, + }); + } + + // 関係ない返信は除外 + if (note.reply && this.user && !this.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + // 流れてきたNoteがミュートすべきNoteだったら無視する + // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) + // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 + // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる + if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + + this.connection.cacheNote(note); + + this.send('note', note); + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} + +@Injectable() +export class HybridAllTimelineChannelService { + public readonly shouldShare = HybridAllTimelineChannel.shouldShare; + public readonly requireCredential = HybridAllTimelineChannel.requireCredential; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): HybridAllTimelineChannel { + return new HybridAllTimelineChannel( + this.metaService, + this.roleService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index d38611c2fc..6cffc44fb7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -130,6 +130,7 @@ "storybook": "7.4.1", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", + "vite-plugin-compression2": "^0.10.4", "vite-plugin-turbosnap": "1.0.3", "vitest": "0.34.4", "vitest-fetch-mock": "0.2.2", diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index afa0004cc7..c702b6403b 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -15,6 +15,7 @@ import { } from 'vue'; import { instanceName } from '@/config'; import { instance as Instance } from '@/instance'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; +import {defaultStore} from "@/store"; const props = defineProps<{ instance?: { diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index cb0aaf085c..52640e7315 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -88,6 +88,15 @@ if (props.src === 'antenna') { withReplies: defaultStore.state.showTimelineReplies, }); connection.on('note', prepend); +} else if (props.src === 'all') { + endpoint = 'notes/hybrid-all-timeline'; + query = { + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('hybridAllTimeline', { + withReplies: defaultStore.state.showTimelineReplies, + }); + connection.on('note', prepend); } else if (props.src === 'global') { endpoint = 'notes/global-timeline'; query = { diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 7c344ccf7c..72768dd171 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only