Merge branch 'develop'
This commit is contained in:
commit
9bde9edcf6
|
@ -130,6 +130,7 @@ proxyBypassHosts:
|
||||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||||
|
|
||||||
# Media Proxy
|
# Media Proxy
|
||||||
|
# Reference Implementation: https://github.com/misskey-dev/media-proxy
|
||||||
#mediaProxy: https://example.com/proxy
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
# Proxy remote files (default: false)
|
# Proxy remote files (default: false)
|
||||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -9,6 +9,17 @@
|
||||||
You should also include the user name that made the change.
|
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)
|
## 13.3.1 (2023/02/04)
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.3.1",
|
"version": "13.3.2",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -87,6 +87,8 @@ export type Mixin = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
clientEntry: string;
|
clientEntry: string;
|
||||||
clientManifestExists: boolean;
|
clientManifestExists: boolean;
|
||||||
|
mediaProxy: string;
|
||||||
|
externalMediaProxyEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = Source & Mixin;
|
export type Config = Source & Mixin;
|
||||||
|
@ -135,6 +137,13 @@ export function loadConfig() {
|
||||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
mixin.clientEntry = clientManifest['src/init.ts'];
|
||||||
mixin.clientManifestExists = clientManifestExists;
|
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;
|
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||||
|
|
||||||
return Object.assign(config, mixin);
|
return Object.assign(config, mixin);
|
||||||
|
|
|
@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
|
||||||
import type { Packed } from '@/misc/schema.js';
|
import type { Packed } from '@/misc/schema.js';
|
||||||
import { DI } from '@/di-symbols.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 { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
export class AntennaService implements OnApplicationShutdown {
|
export class AntennaService implements OnApplicationShutdown {
|
||||||
private antennasFetched: boolean;
|
private antennasFetched: boolean;
|
||||||
private antennas: Antenna[];
|
private antennas: Antenna[];
|
||||||
private blockingCache: Cache<User['id'][]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisSubscriber)
|
||||||
|
@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private antennaEntityService: AntennaEntityService,
|
private antennaEntityService: AntennaEntityService,
|
||||||
) {
|
) {
|
||||||
this.antennasFetched = false;
|
this.antennasFetched = false;
|
||||||
this.antennas = [];
|
this.antennas = [];
|
||||||
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
|
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onRedisMessage);
|
this.redisSubscriber.on('message', this.onRedisMessage);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
read: read,
|
read: read,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
|
|
||||||
if (!read) {
|
if (!read) {
|
||||||
const mutings = await this.mutingsRepository.find({
|
const mutings = await this.mutingsRepository.find({
|
||||||
|
@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
|
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
|
||||||
if (unread) {
|
if (unread) {
|
||||||
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
||||||
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
||||||
antenna: { id: antenna.id, name: antenna.name },
|
antenna: { id: antenna.id, name: antenna.name },
|
||||||
note: await this.noteEntityService.pack(note),
|
note: await this.noteEntityService.pack(note),
|
||||||
|
@ -156,10 +150,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
if (note.visibility === 'specified') return false;
|
if (note.visibility === 'specified') return false;
|
||||||
if (note.visibility === 'followers') 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;
|
if (!antenna.withReplies && note.replyId != null) return false;
|
||||||
|
|
||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class CreateNotificationService {
|
||||||
|
|
||||||
private notificationEntityService: NotificationEntityService,
|
private notificationEntityService: NotificationEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export class CreateNotificationService {
|
||||||
const packed = await this.notificationEntityService.pack(notification, {});
|
const packed = await this.notificationEntityService.pack(notification, {});
|
||||||
|
|
||||||
// Publish notification event
|
// Publish notification event
|
||||||
this.globalEventServie.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
@ -77,7 +77,7 @@ export class CreateNotificationService {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
||||||
|
|
|
@ -120,7 +120,7 @@ export class CustomEmojiService {
|
||||||
const url = isLocal
|
const url = isLocal
|
||||||
? emojiUrl
|
? emojiUrl
|
||||||
: this.config.proxyRemoteFiles
|
: this.config.proxyRemoteFiles
|
||||||
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`
|
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
|
||||||
: emojiUrl;
|
: emojiUrl;
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export class DeleteAccountService {
|
||||||
|
|
||||||
private userSuspendService: UserSuspendService,
|
private userSuspendService: UserSuspendService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,6 @@ export class DeleteAccountService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate streaming
|
// Terminate streaming
|
||||||
this.globalEventServie.publishUserEvent(user.id, 'terminate', {});
|
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ export class NoteCreateService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private createNotificationService: CreateNotificationService,
|
||||||
|
@ -535,7 +535,7 @@ export class NoteCreateService {
|
||||||
// Pack the note
|
// Pack the note
|
||||||
const noteObj = await this.noteEntityService.pack(note);
|
const noteObj = await this.noteEntityService.pack(note);
|
||||||
|
|
||||||
this.globalEventServie.publishNotesStream(noteObj);
|
this.globalEventService.publishNotesStream(noteObj);
|
||||||
|
|
||||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||||
|
@ -561,7 +561,7 @@ export class NoteCreateService {
|
||||||
|
|
||||||
if (!threadMuted) {
|
if (!threadMuted) {
|
||||||
nm.push(data.reply.userId, 'reply');
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -584,7 +584,7 @@ export class NoteCreateService {
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -684,7 +684,7 @@ export class NoteCreateService {
|
||||||
detail: true,
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
|
|
@ -34,7 +34,7 @@ export class NoteDeleteService {
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
@ -63,7 +63,7 @@ export class NoteDeleteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
this.globalEventServie.publishNoteStream(note.id, 'deleted', {
|
this.globalEventService.publishNoteStream(note.id, 'deleted', {
|
||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class NoteReadService {
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private antennaService: AntennaService,
|
private antennaService: AntennaService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
|
@ -87,13 +87,13 @@ export class NoteReadService {
|
||||||
if (exist == null) return;
|
if (exist == null) return;
|
||||||
|
|
||||||
if (params.isMentioned) {
|
if (params.isMentioned) {
|
||||||
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
||||||
}
|
}
|
||||||
if (params.isSpecified) {
|
if (params.isSpecified) {
|
||||||
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||||
}
|
}
|
||||||
if (note.channelId) {
|
if (note.channelId) {
|
||||||
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ export class NoteReadService {
|
||||||
}).then(mentionsCount => {
|
}).then(mentionsCount => {
|
||||||
if (mentionsCount === 0) {
|
if (mentionsCount === 0) {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ export class NoteReadService {
|
||||||
}).then(specifiedCount => {
|
}).then(specifiedCount => {
|
||||||
if (specifiedCount === 0) {
|
if (specifiedCount === 0) {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ export class NoteReadService {
|
||||||
}).then(channelNoteCount => {
|
}).then(channelNoteCount => {
|
||||||
if (channelNoteCount === 0) {
|
if (channelNoteCount === 0) {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventServie.publishMainStream(userId, 'readAllChannels');
|
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -200,14 +200,14 @@ export class NoteReadService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna);
|
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
|
||||||
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
|
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
|
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
|
||||||
if (!unread) {
|
if (!unread) {
|
||||||
this.globalEventServie.publishMainStream(userId, 'readAllAntennas');
|
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
|
||||||
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
|
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 type { Note } from '@/models/entities/Note.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import type { CacheableUser } from '@/models/entities/User.js';
|
import type { CacheableUser } from '@/models/entities/User.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PollService {
|
export class PollService {
|
||||||
|
@ -28,14 +28,11 @@ export class PollService {
|
||||||
@Inject(DI.pollVotesRepository)
|
@Inject(DI.pollVotesRepository)
|
||||||
private pollVotesRepository: PollVotesRepository,
|
private pollVotesRepository: PollVotesRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private userBlockingService: UserBlockingService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
) {
|
) {
|
||||||
|
@ -52,11 +49,8 @@ export class PollService {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
blockerId: note.userId,
|
if (blocked) {
|
||||||
blockeeId: user.id,
|
|
||||||
});
|
|
||||||
if (block) {
|
|
||||||
throw new Error('blocked');
|
throw new Error('blocked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +82,7 @@ export class PollService {
|
||||||
const index = choice + 1; // In SQL, array index is 1 based
|
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}'`);
|
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,
|
choice: choice,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { bindThis } from '@/decorators.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<string, string> = {
|
const legacies: Record<string, string> = {
|
||||||
'like': '👍',
|
'like': '👍',
|
||||||
|
@ -73,8 +74,9 @@ export class ReactionService {
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
private userBlockingService: UserBlockingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private createNotificationService: CreateNotificationService,
|
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) {
|
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) {
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
blockerId: note.userId,
|
if (blocked) {
|
||||||
blockeeId: user.id,
|
|
||||||
});
|
|
||||||
if (block) {
|
|
||||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +156,7 @@ export class ReactionService {
|
||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventServie.publishNoteStream(note.id, 'reacted', {
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: decodedReaction.reaction,
|
reaction: decodedReaction.reaction,
|
||||||
emoji: emoji != null ? {
|
emoji: emoji != null ? {
|
||||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
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);
|
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,
|
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { IdService } from '@/core/IdService.js';
|
||||||
import type { CacheableUser, User } from '@/models/entities/User.js';
|
import type { CacheableUser, User } from '@/models/entities/User.js';
|
||||||
import type { Blocking } from '@/models/entities/Blocking.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 { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||||
import { DI } from '@/di-symbols.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 type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.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 { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Cache } from '@/misc/cache.js';
|
||||||
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserBlockingService {
|
export class UserBlockingService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
|
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||||
|
private blockingsByUserIdCache: Cache<User['id'][]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redisSubscriber)
|
||||||
|
private redisSubscriber: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -42,13 +50,44 @@ export class UserBlockingService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('user-block');
|
this.logger = this.loggerService.getLogger('user-block');
|
||||||
|
|
||||||
|
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
|
||||||
|
|
||||||
|
this.redisSubscriber.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
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
|
@bindThis
|
||||||
|
@ -72,6 +111,11 @@ export class UserBlockingService {
|
||||||
|
|
||||||
await this.blockingsRepository.insert(blocking);
|
await this.blockingsRepository.insert(blocking);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||||
|
blockerId: blocker.id,
|
||||||
|
blockeeId: blockee.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
|
||||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||||
|
@ -97,15 +141,15 @@ export class UserBlockingService {
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isLocalUser(followee)) {
|
||||||
this.userEntityService.pack(followee, followee, {
|
this.userEntityService.pack(followee, followee, {
|
||||||
detail: true,
|
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)) {
|
if (this.userEntityService.isLocalUser(follower)) {
|
||||||
this.userEntityService.pack(followee, follower, {
|
this.userEntityService.pack(followee, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventServie.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -152,8 +196,8 @@ export class UserBlockingService {
|
||||||
this.userEntityService.pack(followee, follower, {
|
this.userEntityService.pack(followee, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventServie.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -210,10 +254,31 @@ export class UserBlockingService {
|
||||||
|
|
||||||
await this.blockingsRepository.delete(blocking.id);
|
await this.blockingsRepository.delete(blocking.id);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||||
|
blockerId: blocker.id,
|
||||||
|
blockeeId: blockee.id,
|
||||||
|
});
|
||||||
|
|
||||||
// deliver if remote bloking
|
// deliver if remote bloking
|
||||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
||||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||||
import { DI } from '@/di-symbols.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
@ -48,21 +49,18 @@ export class UserFollowingService {
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private userBlockingService: UserBlockingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private createNotificationService: CreateNotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
@ -77,26 +75,20 @@ export class UserFollowingService {
|
||||||
|
|
||||||
// check blocking
|
// check blocking
|
||||||
const [blocking, blocked] = await Promise.all([
|
const [blocking, blocked] = await Promise.all([
|
||||||
this.blockingsRepository.findOneBy({
|
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||||
blockerId: follower.id,
|
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
||||||
blockeeId: followee.id,
|
|
||||||
}),
|
|
||||||
this.blockingsRepository.findOneBy({
|
|
||||||
blockerId: followee.id,
|
|
||||||
blockeeId: follower.id,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
|
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));
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||||
this.queueService.deliver(followee, content, follower.inbox);
|
this.queueService.deliver(followee, content, follower.inbox);
|
||||||
return;
|
return;
|
||||||
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
|
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
|
||||||
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
|
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
|
||||||
await this.blockingsRepository.delete(blocking.id);
|
await this.userBlockingService.unblock(follower, followee);
|
||||||
} else {
|
} else {
|
||||||
// それ以外は単純に例外
|
// それ以外は単純に例外
|
||||||
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
|
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
|
||||||
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
|
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, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||||
this.globalEventServie.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -242,7 +234,7 @@ export class UserFollowingService {
|
||||||
// Publish followed event
|
// Publish followed event
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isLocalUser(followee)) {
|
||||||
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -288,8 +280,8 @@ export class UserFollowingService {
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventServie.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
@ -357,14 +349,8 @@ export class UserFollowingService {
|
||||||
|
|
||||||
// check blocking
|
// check blocking
|
||||||
const [blocking, blocked] = await Promise.all([
|
const [blocking, blocked] = await Promise.all([
|
||||||
this.blockingsRepository.findOneBy({
|
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||||
blockerId: follower.id,
|
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
||||||
blockeeId: followee.id,
|
|
||||||
}),
|
|
||||||
this.blockingsRepository.findOneBy({
|
|
||||||
blockerId: followee.id,
|
|
||||||
blockeeId: follower.id,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (blocking != null) throw new Error('blocking');
|
if (blocking != null) throw new Error('blocking');
|
||||||
|
@ -388,11 +374,11 @@ export class UserFollowingService {
|
||||||
|
|
||||||
// Publish receiveRequest event
|
// Publish receiveRequest event
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
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, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
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', {
|
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
|
@ -440,7 +426,7 @@ export class UserFollowingService {
|
||||||
|
|
||||||
this.userEntityService.pack(followee.id, followee, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -468,7 +454,7 @@ export class UserFollowingService {
|
||||||
|
|
||||||
this.userEntityService.pack(followee.id, followee, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -583,8 +569,8 @@ export class UserFollowingService {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||||||
this.globalEventServie.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class UserListService {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private proxyAccountService: ProxyAccountService,
|
private proxyAccountService: ProxyAccountService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export class UserListService {
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as UserListJoining);
|
} 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)) {
|
if (this.userEntityService.isRemoteUser(target)) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class UserMutingService {
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventServie: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class ChannelEntityService {
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
description: channel.description,
|
description: channel.description,
|
||||||
userId: channel.userId,
|
userId: channel.userId,
|
||||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null,
|
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||||
usersCount: channel.usersCount,
|
usersCount: channel.usersCount,
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
|
|
||||||
|
|
|
@ -71,27 +71,41 @@ export class DriveFileEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
return appendQuery(this.config.mediaProxy, query({
|
return proxiedUrl(file.uri);
|
||||||
url: file.uri,
|
|
||||||
thumbnail: thumbnail ? '1' : undefined,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートかつ期限切れはローカルプロキシを試みる
|
// リモートかつ期限切れはローカルプロキシを試みる
|
||||||
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
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('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
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);
|
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
|
@bindThis
|
||||||
|
@ -166,8 +180,8 @@ export class DriveFileEntityService {
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
thumbnailUrl: this.getPublicUrl(file, true),
|
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||||
comment: file.comment,
|
comment: file.comment,
|
||||||
folderId: file.folderId,
|
folderId: file.folderId,
|
||||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||||
|
@ -201,8 +215,8 @@ export class DriveFileEntityService {
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
thumbnailUrl: this.getPublicUrl(file, true),
|
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||||
comment: file.comment,
|
comment: file.comment,
|
||||||
folderId: file.folderId,
|
folderId: file.folderId,
|
||||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||||
|
|
|
@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getAvatarUrl(user: User): Promise<string> {
|
public async getAvatarUrl(user: User): Promise<string> {
|
||||||
if (user.avatar) {
|
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) {
|
} else if (user.avatarId) {
|
||||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: 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 {
|
} else {
|
||||||
return this.getIdenticonUrl(user.id);
|
return this.getIdenticonUrl(user.id);
|
||||||
}
|
}
|
||||||
|
@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@bindThis
|
@bindThis
|
||||||
public getAvatarUrlSync(user: User): string {
|
public getAvatarUrlSync(user: User): string {
|
||||||
if (user.avatar) {
|
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 {
|
} else {
|
||||||
return this.getIdenticonUrl(user.id);
|
return this.getIdenticonUrl(user.id);
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.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,
|
bannerBlurhash: user.banner?.blurhash ?? null,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||||
|
|
||||||
export class Cache<T> {
|
export class Cache<T> {
|
||||||
public cache: Map<string | null, { date: number; value: T; }>;
|
public cache: Map<string | null, { date: number; value: T; }>;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
|
|
|
@ -137,38 +137,38 @@ export class FileServerService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (file.state === 'remote') {
|
if (file.state === 'remote') {
|
||||||
const convertFile = async () => {
|
let image: IImageStreamable | null = null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.fileRole === 'webpublic') {
|
if (file.fileRole === 'thumbnail') {
|
||||||
if (['image/svg+xml'].includes(file.mime)) {
|
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
|
||||||
return this.imageProcessingService.convertToWebpStream(
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
file.path,
|
|
||||||
2048,
|
|
||||||
2048,
|
|
||||||
{ ...webpDefault, lossless: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
data: fs.createReadStream(file.path),
|
||||||
ext: file.ext,
|
ext: file.ext,
|
||||||
type: file.mime,
|
type: file.mime,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const image = await convertFile();
|
|
||||||
|
|
||||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||||
// image.dataがstreamなら、stream終了後にcleanup
|
// 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('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
|
||||||
return image.data;
|
return image.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,6 +216,23 @@ export class FileServerService {
|
||||||
return;
|
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
|
// Create temp file
|
||||||
const file = await this.getStreamAndTypeFromUrl(url);
|
const file = await this.getStreamAndTypeFromUrl(url);
|
||||||
if (file === '404') {
|
if (file === '404') {
|
||||||
|
@ -236,7 +252,7 @@ export class FileServerService {
|
||||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||||
|
|
||||||
let image: IImageStreamable | null = null;
|
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)) {
|
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||||
image = {
|
image = {
|
||||||
data: fs.createReadStream(file.path),
|
data: fs.createReadStream(file.path),
|
||||||
|
@ -246,7 +262,7 @@ export class FileServerService {
|
||||||
} else {
|
} else {
|
||||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
const data = sharp(file.path, { animated: !('static' in request.query) })
|
||||||
.resize({
|
.resize({
|
||||||
height: 128,
|
height: 'emoji' in request.query ? 128 : 320,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.webp(webpDefault);
|
.webp(webpDefault);
|
||||||
|
@ -370,7 +386,7 @@ export class FileServerService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getFileFromKey(key: string): Promise<
|
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; }
|
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||||
| '404'
|
| '404'
|
||||||
| '204'
|
| '204'
|
||||||
|
@ -392,6 +408,7 @@ export class FileServerService {
|
||||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
url: file.uri,
|
||||||
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||||
file,
|
file,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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なので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||||
url.searchParams.set('emoji', '1');
|
url.searchParams.set('emoji', '1');
|
||||||
|
|
|
@ -181,6 +181,10 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
mediaProxy: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
features: {
|
features: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
@ -307,6 +311,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||||
|
|
||||||
|
mediaProxy: this.config.mediaProxy,
|
||||||
|
|
||||||
...(ps.detail ? {
|
...(ps.detail ? {
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedPages: instance.pinnedPages,
|
||||||
pinnedClipId: instance.pinnedClipId,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
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 type { IRemoteUser } from '@/models/entities/User.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.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 { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -77,9 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
@ -93,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private createNotificationService: CreateNotificationService,
|
||||||
|
private userBlockingService: UserBlockingService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
|
@ -109,11 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== me.id) {
|
if (note.userId !== me.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
|
||||||
blockerId: note.userId,
|
if (blocked) {
|
||||||
blockeeId: me.id,
|
|
||||||
});
|
|
||||||
if (block) {
|
|
||||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ export interface InternalStreamTypes {
|
||||||
remoteUserUpdated: { id: User['id']; };
|
remoteUserUpdated: { id: User['id']; };
|
||||||
follow: { followerId: User['id']; followeeId: User['id']; };
|
follow: { followerId: User['id']; followeeId: User['id']; };
|
||||||
unfollow: { 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'];
|
policiesUpdated: Role['policies'];
|
||||||
roleCreated: Role;
|
roleCreated: Role;
|
||||||
roleDeleted: Role;
|
roleDeleted: Role;
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class UrlPreviewService {
|
||||||
private wrap(url?: string): string | null {
|
private wrap(url?: string): string | null {
|
||||||
return url != null
|
return url != null
|
||||||
? url.match(/^https?:\/\//)
|
? url.match(/^https?:\/\//)
|
||||||
? `${this.config.url}/proxy/preview.webp?${query({
|
? `${this.config.mediaProxy}/preview.webp?${query({
|
||||||
url,
|
url,
|
||||||
preview: '1',
|
preview: '1',
|
||||||
})}`
|
})}`
|
||||||
|
@ -74,6 +74,14 @@ export class UrlPreviewService {
|
||||||
|
|
||||||
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
|
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.icon = this.wrap(summary.icon);
|
||||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
<div v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||||
<button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
|
<button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
|
||||||
<iframe :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
<iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||||
|
<span v-else>invalid url</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" :class="$style.twitter">
|
<div v-else-if="tweetId && tweetExpanded" ref="twitter" :class="$style.twitter">
|
||||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
|
|
||||||
<div class="poamfof">
|
<div class="poamfof">
|
||||||
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||||
<div v-if="player.url" class="player">
|
<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
|
||||||
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-else>invalid url</span>
|
||||||
</Transition>
|
</Transition>
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<MkError v-else-if="!player.url" @retry="ytFetch()"/>
|
<MkError v-else-if="!player.url" @retry="ytFetch()"/>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { query, appendQuery } from '@/scripts/url';
|
import { query, appendQuery } from '@/scripts/url';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
|
||||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string {
|
export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string {
|
||||||
if (imageUrl.startsWith(`${url}/proxy/`) || imageUrl.startsWith('/proxy/')) {
|
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/')) {
|
||||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||||
return appendQuery(imageUrl, query({
|
return appendQuery(imageUrl, query({
|
||||||
fallback: '1',
|
fallback: '1',
|
||||||
|
@ -10,7 +11,7 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${url}/proxy/image.webp?${query({
|
return `${instance.mediaProxy}/image.webp?${query({
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
fallback: '1',
|
fallback: '1',
|
||||||
...(type ? { [type]: '1' } : {}),
|
...(type ? { [type]: '1' } : {}),
|
||||||
|
@ -25,22 +26,19 @@ export function getProxiedImageUrlNullable(imageUrl: string | null | undefined,
|
||||||
export function getStaticImageUrl(baseUrl: string): string {
|
export function getStaticImageUrl(baseUrl: string): string {
|
||||||
const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
|
const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
|
||||||
|
|
||||||
if (u.href.startsWith(`${url}/proxy/`)) {
|
|
||||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
|
||||||
u.searchParams.set('static', '1');
|
|
||||||
return u.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (u.href.startsWith(`${url}/emoji/`)) {
|
if (u.href.startsWith(`${url}/emoji/`)) {
|
||||||
// もう既にemojiっぽそうだったらsearchParams付けるだけ
|
// もう既にemojiっぽそうだったらsearchParams付けるだけ
|
||||||
u.searchParams.set('static', '1');
|
u.searchParams.set('static', '1');
|
||||||
return u.href;
|
return u.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する
|
if (u.href.startsWith(instance.mediaProxy + '/')) {
|
||||||
const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`;
|
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||||
|
u.searchParams.set('static', '1');
|
||||||
|
return u.href;
|
||||||
|
}
|
||||||
|
|
||||||
return `${url}/proxy/${dummy}?${query({
|
return `${instance.mediaProxy}/static.webp?${query({
|
||||||
url: u.href,
|
url: u.href,
|
||||||
static: '1',
|
static: '1',
|
||||||
})}`;
|
})}`;
|
||||||
|
|
Loading…
Reference in New Issue