diff --git a/.config/example.yml b/.config/example.yml index 8fe41da15a..a19b5d04e8 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -130,6 +130,7 @@ proxyBypassHosts: #proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 # Media Proxy +# Reference Implementation: https://github.com/misskey-dev/media-proxy #mediaProxy: https://example.com/proxy # Proxy remote files (default: false) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e10edf683..7afaaffae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ You should also include the user name that made the change. --> +## 13.3.2 (2023/02/04) + +### Improvements +- 外部メディアプロキシへの対応を強化しました + 外部メディアプロキシのFastify実装を作りました + https://github.com/misskey-dev/media-proxy +- Server: improve performance + +### Bugfixes +- Client: validate urls to improve security + ## 13.3.1 (2023/02/04) ### Bugfixes diff --git a/package.json b/package.json index 532ba6570f..236d02eb3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.3.1", + "version": "13.3.2", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 1d4e700656..aa98ef1d22 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -87,6 +87,8 @@ export type Mixin = { userAgent: string; clientEntry: string; clientManifestExists: boolean; + mediaProxy: string; + externalMediaProxyEnabled: boolean; }; export type Config = Source & Mixin; @@ -135,6 +137,13 @@ export function loadConfig() { mixin.clientEntry = clientManifest['src/init.ts']; mixin.clientManifestExists = clientManifestExists; + const externalMediaProxy = config.mediaProxy ? + config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy + : null; + const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`; + mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; + mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 7db8c43ea5..a71327e947 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; -import { Cache } from '@/misc/cache.js'; import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common'; export class AntennaService implements OnApplicationShutdown { private antennasFetched: boolean; private antennas: Antenna[]; - private blockingCache: Cache; constructor( @Inject(DI.redisSubscriber) @@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown { private utilityService: UtilityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, private noteEntityService: NoteEntityService, private antennaEntityService: AntennaEntityService, ) { this.antennasFetched = false; this.antennas = []; - this.blockingCache = new Cache(1000 * 60 * 5); this.redisSubscriber.on('message', this.onRedisMessage); } @@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown { read: read, }); - this.globalEventServie.publishAntennaStream(antenna.id, 'note', note); + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); if (!read) { const mutings = await this.mutingsRepository.find({ @@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown { setTimeout(async () => { const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); if (unread) { - this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); + this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { antenna: { id: antenna.id, name: antenna.name }, note: await this.noteEntityService.pack(note), @@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; - - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); - if (blockings.some(blocking => blocking === antenna.userId)) return false; if (!antenna.withReplies && note.replyId != null) return false; diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index f376b7b9cf..cd47844a75 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -26,7 +26,7 @@ export class CreateNotificationService { private notificationEntityService: NotificationEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, ) { } @@ -60,7 +60,7 @@ export class CreateNotificationService { const packed = await this.notificationEntityService.pack(notification, {}); // Publish notification event - this.globalEventServie.publishMainStream(notifieeId, 'notification', packed); + this.globalEventService.publishMainStream(notifieeId, 'notification', packed); // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { @@ -77,7 +77,7 @@ export class CreateNotificationService { } //#endregion - this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed); + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 39814e1be6..63f0319442 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -120,7 +120,7 @@ export class CustomEmojiService { const url = isLocal ? emojiUrl : this.config.proxyRemoteFiles - ? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}` + ? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}` : emojiUrl; return url; diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 0ac12857c9..2acb5f2303 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -14,7 +14,7 @@ export class DeleteAccountService { private userSuspendService: UserSuspendService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, ) { } @@ -38,6 +38,6 @@ export class DeleteAccountService { }); // Terminate streaming - this.globalEventServie.publishUserEvent(user.id, 'terminate', {}); + this.globalEventService.publishUserEvent(user.id, 'terminate', {}); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3dc44a25fe..4a81f764dc 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -175,7 +175,7 @@ export class NoteCreateService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private queueService: QueueService, private noteReadService: NoteReadService, private createNotificationService: CreateNotificationService, @@ -535,7 +535,7 @@ export class NoteCreateService { // Pack the note const noteObj = await this.noteEntityService.pack(note); - this.globalEventServie.publishNotesStream(noteObj); + this.globalEventService.publishNotesStream(noteObj); this.webhookService.getActiveWebhooks().then(webhooks => { webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); @@ -561,7 +561,7 @@ export class NoteCreateService { if (!threadMuted) { nm.push(data.reply.userId, 'reply'); - this.globalEventServie.publishMainStream(data.reply.userId, 'reply', noteObj); + this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); for (const webhook of webhooks) { @@ -584,7 +584,7 @@ export class NoteCreateService { // Publish event if ((user.id !== data.renote.userId) && data.renote.userHost === null) { - this.globalEventServie.publishMainStream(data.renote.userId, 'renote', noteObj); + this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); for (const webhook of webhooks) { @@ -684,7 +684,7 @@ export class NoteCreateService { detail: true, }); - this.globalEventServie.publishMainStream(u.id, 'mention', detailPackedNote); + this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); for (const webhook of webhooks) { diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index b1f16b6e8a..4dad825097 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -34,7 +34,7 @@ export class NoteDeleteService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, @@ -63,7 +63,7 @@ export class NoteDeleteService { } if (!quiet) { - this.globalEventServie.publishNoteStream(note.id, 'deleted', { + this.globalEventService.publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt, }); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index f4395725d6..84983d600e 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -40,7 +40,7 @@ export class NoteReadService { private userEntityService: UserEntityService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private notificationService: NotificationService, private antennaService: AntennaService, private pushNotificationService: PushNotificationService, @@ -87,13 +87,13 @@ export class NoteReadService { if (exist == null) return; if (params.isMentioned) { - this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); + this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); } if (params.isSpecified) { - this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); + this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); } if (note.channelId) { - this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); + this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id); } }, 2000); } @@ -155,7 +155,7 @@ export class NoteReadService { }).then(mentionsCount => { if (mentionsCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); + this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } }); @@ -165,7 +165,7 @@ export class NoteReadService { }).then(specifiedCount => { if (specifiedCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } }); @@ -175,7 +175,7 @@ export class NoteReadService { }).then(channelNoteCount => { if (channelNoteCount === 0) { // 全て既読になったイベントを発行 - this.globalEventServie.publishMainStream(userId, 'readAllChannels'); + this.globalEventService.publishMainStream(userId, 'readAllChannels'); } }); @@ -200,14 +200,14 @@ export class NoteReadService { }); if (count === 0) { - this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); + this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); } } this.userEntityService.getHasUnreadAntenna(userId).then(unread => { if (!unread) { - this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); + this.globalEventService.publishMainStream(userId, 'readAllAntennas'); this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined); } }); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index abc598ab76..042dcb3e67 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,17 +1,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; import type { CacheableUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { bindThis } from '@/decorators.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; @Injectable() export class PollService { @@ -28,14 +28,11 @@ export class PollService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - private userEntityService: UserEntityService, private idService: IdService, private relayService: RelayService, - private globalEventServie: GlobalEventService, - private createNotificationService: CreateNotificationService, + private globalEventService: GlobalEventService, + private userBlockingService: UserBlockingService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, ) { @@ -52,11 +49,8 @@ export class PollService { // Check blocking if (note.userId !== user.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); + if (blocked) { throw new Error('blocked'); } } @@ -88,7 +82,7 @@ export class PollService { const index = choice + 1; // In SQL, array index is 1 based await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - this.globalEventServie.publishNoteStream(note.id, 'pollVoted', { + this.globalEventService.publishNoteStream(note.id, 'pollVoted', { choice: choice, userId: user.id, }); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 0c1c3d0a3b..3806590059 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; -import { UtilityService } from './UtilityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; const legacies: Record = { 'like': '👍', @@ -73,8 +74,9 @@ export class ReactionService { private metaService: MetaService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, + private userBlockingService: UserBlockingService, private idService: IdService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private createNotificationService: CreateNotificationService, @@ -86,11 +88,8 @@ export class ReactionService { public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) { // Check blocking if (note.userId !== user.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); + if (blocked) { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } @@ -157,7 +156,7 @@ export class ReactionService { select: ['name', 'host', 'originalUrl', 'publicUrl'], }); - this.globalEventServie.publishNoteStream(note.id, 'reacted', { + this.globalEventService.publishNoteStream(note.id, 'reacted', { reaction: decodedReaction.reaction, emoji: emoji != null ? { name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, @@ -229,7 +228,7 @@ export class ReactionService { if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - this.globalEventServie.publishNoteStream(note.id, 'unreacted', { + this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, }); diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index c923700427..d734328669 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,5 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import Redis from 'ioredis'; import { IdService } from '@/core/IdService.js'; import type { CacheableUser, User } from '@/models/entities/User.js'; import type { Blocking } from '@/models/entities/Blocking.js'; @@ -7,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { DI } from '@/di-symbols.js'; -import logger from '@/logger.js'; import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -15,12 +15,20 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; +import { Cache } from '@/misc/cache.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; @Injectable() -export class UserBlockingService { +export class UserBlockingService implements OnApplicationShutdown { private logger: Logger; + // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ + private blockingsByUserIdCache: Cache; + constructor( + @Inject(DI.redisSubscriber) + private redisSubscriber: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,13 +50,44 @@ export class UserBlockingService { private userEntityService: UserEntityService, private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private webhookService: WebhookService, private apRendererService: ApRendererService, private perUserFollowingChart: PerUserFollowingChart, private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('user-block'); + + this.blockingsByUserIdCache = new Cache(Infinity); + + this.redisSubscriber.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as StreamMessages['internal']['payload']; + switch (type) { + case 'blockingCreated': { + const cached = this.blockingsByUserIdCache.get(body.blockerId); + if (cached) { + this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]); + } + break; + } + case 'blockingDeleted': { + const cached = this.blockingsByUserIdCache.get(body.blockerId); + if (cached) { + this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId)); + } + break; + } + default: + break; + } + } } @bindThis @@ -72,6 +111,11 @@ export class UserBlockingService { await this.blockingsRepository.insert(blocking); + this.globalEventService.publishInternalEvent('blockingCreated', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); this.queueService.deliver(blocker, content, blockee.inbox); @@ -97,15 +141,15 @@ export class UserBlockingService { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(followee, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } if (this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -152,8 +196,8 @@ export class UserBlockingService { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -210,10 +254,31 @@ export class UserBlockingService { await this.blockingsRepository.delete(blocking.id); + this.globalEventService.publishInternalEvent('blockingDeleted', { + blockerId: blocker.id, + blockeeId: blockee.id, + }); + // deliver if remote bloking if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); this.queueService.deliver(blocker, content, blockee.inbox); } } + + @bindThis + public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise { + const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({ + where: { + blockerId, + }, + select: ['blockeeId'], + }).then(records => records.map(record => record.blockeeId))); + return blockedUserIds.includes(blockeeId); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index f1ce311cea..18a2ef5c05 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -48,21 +49,18 @@ export class UserFollowingService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, + private userBlockingService: UserBlockingService, private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private createNotificationService: CreateNotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, - private globalEventService: GlobalEventService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -77,26 +75,20 @@ export class UserFollowingService { // check blocking const [blocking, blocked] = await Promise.all([ - this.blockingsRepository.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - this.blockingsRepository.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), + this.userBlockingService.checkBlocked(follower.id, followee.id), + this.userBlockingService.checkBlocked(followee.id, follower.id), ]); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { - // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 + // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); return; } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { - // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 - await this.blockingsRepository.delete(blocking.id); + // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 + await this.userBlockingService.unblock(follower, followee); } else { - // それ以外は単純に例外 + // それ以外は単純に例外 if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } @@ -227,8 +219,8 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); - this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); + this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); for (const webhook of webhooks) { @@ -242,7 +234,7 @@ export class UserFollowingService { // Publish followed event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { - this.globalEventServie.publishMainStream(followee.id, 'followed', packed); + this.globalEventService.publishMainStream(followee.id, 'followed', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); for (const webhook of webhooks) { @@ -288,8 +280,8 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { @@ -357,14 +349,8 @@ export class UserFollowingService { // check blocking const [blocking, blocked] = await Promise.all([ - this.blockingsRepository.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - this.blockingsRepository.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), + this.userBlockingService.checkBlocked(follower.id, followee.id), + this.userBlockingService.checkBlocked(followee.id, follower.id), ]); if (blocking != null) throw new Error('blocking'); @@ -388,11 +374,11 @@ export class UserFollowingService { // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { - this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed)); + this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { @@ -440,7 +426,7 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis @@ -468,7 +454,7 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, followee, { detail: true, - }).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); + }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @bindThis @@ -583,8 +569,8 @@ export class UserFollowingService { detail: true, }); - this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee); - this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee); + this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee); + this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index fc48738307..c174394999 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -25,7 +25,7 @@ export class UserListService { private idService: IdService, private userFollowingService: UserFollowingService, private roleService: RoleService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, ) { } @@ -46,7 +46,7 @@ export class UserListService { userListId: list.id, } as UserListJoining); - this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); + this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする if (this.userEntityService.isRemoteUser(target)) { diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 3029d02c00..e98f11709f 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -18,7 +18,7 @@ export class UserMutingService { private idService: IdService, private queueService: QueueService, - private globalEventServie: GlobalEventService, + private globalEventService: GlobalEventService, ) { } diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 5e2f019a12..6ce590aa96 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -54,7 +54,7 @@ export class ChannelEntityService { name: channel.name, description: channel.description, userId: channel.userId, - bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null, + bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null, usersCount: channel.usersCount, notesCount: channel.notesCount, diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 7f54cfdeac..efc196f74a 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -71,27 +71,41 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, thumbnail = false): string | null { + public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail + const proxiedUrl = (url: string) => appendQuery( + `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, + query({ + url, + ...(mode ? { [mode]: '1' } : {}), + }) + ); + // リモートかつメディアプロキシ - if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) { - return appendQuery(this.config.mediaProxy, query({ - url: file.uri, - thumbnail: thumbnail ? '1' : undefined, - })); + if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + return proxiedUrl(file.uri); } // リモートかつ期限切れはローカルプロキシを試みる if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { - const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey; + const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 - return `${this.config.url}/files/${key}`; + const url = `${this.config.url}/files/${key}`; + if (mode === 'avatar') return proxiedUrl(url); + return url; } } const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type); - return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); + if (mode === 'static') { + return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null); + } + + const url = file.webpublicUrl ?? file.url; + + if (mode === 'avatar') return proxiedUrl(url); + return url; } @bindThis @@ -166,8 +180,8 @@ export class DriveFileEntityService { isSensitive: file.isSensitive, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), + url: opts.self ? file.url : this.getPublicUrl(file), + thumbnailUrl: this.getPublicUrl(file, 'static'), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { @@ -201,8 +215,8 @@ export class DriveFileEntityService { isSensitive: file.isSensitive, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), + url: opts.self ? file.url : this.getPublicUrl(file), + thumbnailUrl: this.getPublicUrl(file, 'static'), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index aaa80033b3..ff42c07359 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getAvatarUrl(user: User): Promise { if (user.avatar) { - return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else if (user.avatarId) { const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); - return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else { return this.getIdenticonUrl(user.id); } @@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit { @bindThis public getAvatarUrlSync(user: User): string { if (user.avatar) { - return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); + return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id); } else { return this.getIdenticonUrl(user.id); } @@ -422,7 +422,7 @@ export class UserEntityService implements OnModuleInit { createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, + bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null, bannerBlurhash: user.banner?.blurhash ?? null, isLocked: user.isLocked, isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 69512498f8..43a71a2b57 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,5 +1,7 @@ import { bindThis } from '@/decorators.js'; +// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? + export class Cache { public cache: Map; private lifetime: number; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 40024270ae..39bc4c1d96 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -137,38 +137,38 @@ export class FileServerService { try { if (file.state === 'remote') { - const convertFile = async () => { - if (file.fileRole === 'thumbnail') { - if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) { - return this.imageProcessingService.convertToWebpStream( - file.path, - 498, - 280 - ); - } else if (file.mime.startsWith('video/')) { - return await this.videoProcessingService.generateVideoThumbnail(file.path); - } - } + let image: IImageStreamable | null = null; - if (file.fileRole === 'webpublic') { - if (['image/svg+xml'].includes(file.mime)) { - return this.imageProcessingService.convertToWebpStream( - file.path, - 2048, - 2048, - { ...webpDefault, lossless: true } - ) - } - } + if (file.fileRole === 'thumbnail') { + if (isMimeImage(file.mime, 'sharp-convertible-image')) { + reply.header('Cache-Control', 'max-age=31536000, immutable'); - return { + const url = new URL(`${this.config.mediaProxy}/static.webp`); + url.searchParams.set('url', file.url); + url.searchParams.set('static', '1'); + return await reply.redirect(301, url.toString()); + } else if (file.mime.startsWith('video/')) { + image = await this.videoProcessingService.generateVideoThumbnail(file.path); + } + } + + if (file.fileRole === 'webpublic') { + if (['image/svg+xml'].includes(file.mime)) { + reply.header('Cache-Control', 'max-age=31536000, immutable'); + + const url = new URL(`${this.config.mediaProxy}/svg.webp`); + url.searchParams.set('url', file.url); + return await reply.redirect(301, url.toString()); + } + } + + if (!image) { + image = { data: fs.createReadStream(file.path), ext: file.ext, type: file.mime, }; - }; - - const image = await convertFile(); + } if ('pipe' in image.data && typeof image.data.pipe === 'function') { // image.dataがstreamなら、stream終了後にcleanup @@ -180,7 +180,6 @@ export class FileServerService { } reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); return image.data; } @@ -217,6 +216,23 @@ export class FileServerService { return; } + if (this.config.externalMediaProxyEnabled) { + // 外部のメディアプロキシが有効なら、そちらにリダイレクト + + reply.header('Cache-Control', 'public, max-age=259200'); // 3 days + + const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`); + + for (const [key, value] of Object.entries(request.query)) { + url.searchParams.append(key, value); + } + + return await reply.redirect( + 301, + url.toString(), + ); + } + // Create temp file const file = await this.getStreamAndTypeFromUrl(url); if (file === '404') { @@ -236,7 +252,7 @@ export class FileServerService { const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image'); let image: IImageStreamable | null = null; - if ('emoji' in request.query && isConvertibleImage) { + if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) { if (!isAnimationConvertibleImage && !('static' in request.query)) { image = { data: fs.createReadStream(file.path), @@ -246,7 +262,7 @@ export class FileServerService { } else { const data = sharp(file.path, { animated: !('static' in request.query) }) .resize({ - height: 128, + height: 'emoji' in request.query ? 128 : 320, withoutEnlargement: true, }) .webp(webpDefault); @@ -370,7 +386,7 @@ export class FileServerService { @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } | '404' | '204' @@ -392,6 +408,7 @@ export class FileServerService { const result = await this.downloadAndDetectTypeFromUrl(file.uri); return { ...result, + url: file.uri, fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', file, } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index beb3a34ecd..c7a2c99f94 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -106,7 +106,7 @@ export class ServerService { } } - const url = new URL('/proxy/emoji.webp', this.config.url); + const url = new URL(`${this.config.mediaProxy}/emoji.webp`); // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('emoji', '1'); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 3baf945323..2fa7a09d49 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -181,6 +181,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + mediaProxy: { + type: 'string', + optional: false, nullable: false, + }, features: { type: 'object', optional: true, nullable: false, @@ -307,6 +311,8 @@ export default class extends Endpoint { policies: { ...DEFAULT_POLICIES, ...instance.policies }, + mediaProxy: this.config.mediaProxy, + ...(ps.detail ? { pinnedPages: instance.pinnedPages, pinnedClipId: instance.pinnedClipId, diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index d583dfb936..befaea4664 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,6 +1,6 @@ import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { IRemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -77,9 +78,6 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -93,6 +91,7 @@ export default class extends Endpoint { private apRendererService: ApRendererService, private globalEventService: GlobalEventService, private createNotificationService: CreateNotificationService, + private userBlockingService: UserBlockingService, ) { super(meta, paramDef, async (ps, me) => { const createdAt = new Date(); @@ -109,11 +108,8 @@ export default class extends Endpoint { // Check blocking if (note.userId !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: note.userId, - blockeeId: me.id, - }); - if (block) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id); + if (blocked) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 36bfa78363..8bb4147b43 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -25,6 +25,8 @@ export interface InternalStreamTypes { remoteUserUpdated: { id: User['id']; }; follow: { followerId: User['id']; followeeId: User['id']; }; unfollow: { followerId: User['id']; followeeId: User['id']; }; + blockingCreated: { blockerId: User['id']; blockeeId: User['id']; }; + blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; }; policiesUpdated: Role['policies']; roleCreated: Role; roleDeleted: Role; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 802b404ce6..57461b7a33 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -33,7 +33,7 @@ export class UrlPreviewService { private wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) - ? `${this.config.url}/proxy/preview.webp?${query({ + ? `${this.config.mediaProxy}/preview.webp?${query({ url, preview: '1', })}` @@ -73,6 +73,14 @@ export class UrlPreviewService { }); this.logger.succ(`Got preview of ${url}: ${summary.title}`); + + if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { + throw new Error('unsupported schema included'); + } + + if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { + throw new Error('unsupported schema included'); + } summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index f7677faf74..62e58e1553 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -1,7 +1,8 @@