summaryrefslogtreecommitdiff
path: root/packages/backend/src/core
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-10-09 15:17:22 +0100
committerdakkar <dakkar@thenautilus.net>2024-10-09 15:17:22 +0100
commitf00576bce6b5f4086112f48046316bfe49559759 (patch)
tree9268031a42551f3bfafbb33091f925e0cb5af3aa /packages/backend/src/core
parentMerge branch 'merge-requests/668' into feature/2024.9.0 (diff)
parentMerge pull request #14580 from misskey-dev/develop (diff)
downloadsharkey-f00576bce6b5f4086112f48046316bfe49559759.tar.gz
sharkey-f00576bce6b5f4086112f48046316bfe49559759.tar.bz2
sharkey-f00576bce6b5f4086112f48046316bfe49559759.zip
Merge remote-tracking branch 'misskey/master' into feature/2024.9.0
Diffstat (limited to 'packages/backend/src/core')
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts12
-rw-r--r--packages/backend/src/core/AccountMoveService.ts9
-rw-r--r--packages/backend/src/core/AntennaService.ts13
-rw-r--r--packages/backend/src/core/CoreModule.ts12
-rw-r--r--packages/backend/src/core/DownloadService.ts2
-rw-r--r--packages/backend/src/core/DriveService.ts42
-rw-r--r--packages/backend/src/core/EmailService.ts64
-rw-r--r--packages/backend/src/core/GlobalEventService.ts2
-rw-r--r--packages/backend/src/core/HashtagService.ts12
-rw-r--r--packages/backend/src/core/MetaService.ts4
-rw-r--r--packages/backend/src/core/MfmService.ts4
-rw-r--r--packages/backend/src/core/NoteCreateService.ts87
-rw-r--r--packages/backend/src/core/NoteDeleteService.ts15
-rw-r--r--packages/backend/src/core/ProxyAccountService.ts13
-rw-r--r--packages/backend/src/core/PushNotificationService.ts16
-rw-r--r--packages/backend/src/core/QueueService.ts28
-rw-r--r--packages/backend/src/core/ReactionService.ts93
-rw-r--r--packages/backend/src/core/ReactionsBufferingService.ts211
-rw-r--r--packages/backend/src/core/RoleService.ts25
-rw-r--r--packages/backend/src/core/SignupService.ts10
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts13
-rw-r--r--packages/backend/src/core/UserFollowingService.ts35
-rw-r--r--packages/backend/src/core/UserWebhookService.ts29
-rw-r--r--packages/backend/src/core/UtilityService.ts18
-rw-r--r--packages/backend/src/core/WebAuthnService.ts110
-rw-r--r--packages/backend/src/core/WebhookTestService.ts435
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts12
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts1
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts10
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts14
-rw-r--r--packages/backend/src/core/activitypub/misc/contexts.ts1
-rw-r--r--packages/backend/src/core/activitypub/models/ApImageService.ts11
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts13
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts24
-rw-r--r--packages/backend/src/core/activitypub/type.ts1
-rw-r--r--packages/backend/src/core/chart/charts/federation.ts19
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts16
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts10
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts68
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts11
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts4
41 files changed, 1200 insertions, 329 deletions
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 7be5335885..fe2c63e7d6 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -14,10 +14,10 @@ import type {
AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient,
MiAbuseUserReport,
+ MiMeta,
MiUser,
} from '@/models/_.js';
import { EmailService } from '@/core/EmailService.js';
-import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -27,15 +27,19 @@ import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportNotificationService implements OnApplicationShutdown {
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.abuseReportNotificationRecipientRepository)
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
+
private idService: IdService,
private roleService: RoleService,
private systemWebhookService: SystemWebhookService,
private emailService: EmailService,
- private metaService: MetaService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
@@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null),
);
- // 送信先の鮮度を保つため、毎回取得する
- const meta = await this.metaService.fetch(true);
recipientEMailAddresses.push(
- ...(meta.email ? [meta.email] : []),
+ ...(this.meta.email ? [this.meta.email] : []),
);
if (recipientEMailAddresses.length <= 0) {
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index b6b591d240..6e3125044c 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import { IdService } from '@/core/IdService.js';
@@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { MetaService } from '@/core/MetaService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@Injectable()
export class AccountMoveService {
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -57,7 +59,6 @@ export class AccountMoveService {
private perUserFollowingChart: PerUserFollowingChart,
private federatedInstanceService: FederatedInstanceService,
private instanceChart: InstanceChart,
- private metaService: MetaService,
private relayService: RelayService,
private queueService: QueueService,
) {
@@ -276,7 +277,7 @@ export class AccountMoveService {
if (this.userEntityService.isRemoteUser(oldAccount)) {
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 89e475b5f1..ec9ace417e 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
- const listUsers = (await this.userListMembershipsRepository.findBy({
- userListId: antenna.userListId!,
- })).map(x => x.userId);
-
- if (!listUsers.includes(note.userId)) return false;
+ if (antenna.userListId == null) return false;
+ const exists = await this.userListMembershipsRepository.exists({
+ where: {
+ userListId: antenna.userListId,
+ userId: note.userId,
+ },
+ });
+ if (!exists) return false;
} else if (antenna.src === 'users') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 049d858189..a192c2f270 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -13,6 +13,7 @@ import {
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AnnouncementService } from './AnnouncementService.js';
@@ -49,6 +50,7 @@ import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js';
import { QueryService } from './QueryService.js';
import { ReactionService } from './ReactionService.js';
+import { ReactionsBufferingService } from './ReactionsBufferingService.js';
import { RelayService } from './RelayService.js';
import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js';
@@ -193,6 +195,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
+const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
@@ -212,6 +215,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
+const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -343,6 +347,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
PushNotificationService,
QueryService,
ReactionService,
+ ReactionsBufferingService,
RelayService,
RoleService,
S3Service,
@@ -362,6 +367,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -489,6 +495,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$PushNotificationService,
$QueryService,
$ReactionService,
+ $ReactionsBufferingService,
$RelayService,
$RoleService,
$S3Service,
@@ -508,6 +515,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -636,6 +644,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
PushNotificationService,
QueryService,
ReactionService,
+ ReactionsBufferingService,
RelayService,
RoleService,
S3Service,
@@ -655,6 +664,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -781,6 +791,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$PushNotificationService,
$QueryService,
$ReactionService,
+ $ReactionsBufferingService,
$RelayService,
$RoleService,
$S3Service,
@@ -800,6 +811,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts
index 83452845d4..0e992f05de 100644
--- a/packages/backend/src/core/DownloadService.ts
+++ b/packages/backend/src/core/DownloadService.ts
@@ -42,7 +42,7 @@ export class DownloadService {
const timeout = options.timeout ?? 30 * 1000;
const operationTimeout = options.operationTimeout ?? 60 * 1000;
- const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000;
+ const maxSize = options.maxSize ?? this.config.maxFileSize;
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 46fa4243a7..744bc65be7 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import Logger from '@/logger.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
@@ -99,6 +98,9 @@ export class DriveService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -115,7 +117,6 @@ export class DriveService {
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
- private metaService: MetaService,
private downloadService: DownloadService,
private internalStorageService: InternalStorageService,
private s3Service: S3Service,
@@ -149,9 +150,7 @@ export class DriveService {
// thunbnail, webpublic を必要なら生成
const alts = await this.generateAlts(path, type, !file.uri);
- const meta = await this.metaService.fetch();
-
- if (meta.useObjectStorage) {
+ if (this.meta.useObjectStorage) {
//#region ObjectStorage params
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
@@ -170,11 +169,11 @@ export class DriveService {
ext = '';
}
- const baseUrl = meta.objectStorageBaseUrl
- ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
+ const baseUrl = this.meta.objectStorageBaseUrl
+ ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
// for original
- const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
+ const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`;
// for alts
@@ -191,7 +190,7 @@ export class DriveService {
];
if (alts.webpublic) {
- webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
+ webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@@ -199,7 +198,7 @@ export class DriveService {
}
if (alts.thumbnail) {
- thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
+ thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
@@ -376,10 +375,8 @@ export class DriveService {
if (type === 'image/apng') type = 'image/png';
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
- const meta = await this.metaService.fetch();
-
const params = {
- Bucket: meta.objectStorageBucket,
+ Bucket: this.meta.objectStorageBucket,
Key: key,
Body: stream,
ContentType: type,
@@ -392,9 +389,9 @@ export class DriveService {
// 許可されているファイル形式でしか拡張子をつけない
ext ? correctFilename(filename, ext) : filename,
);
- if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
+ if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
- await this.s3Service.upload(meta, params)
+ await this.s3Service.upload(this.meta, params)
.then(
result => {
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
@@ -463,9 +460,7 @@ export class DriveService {
requestHeaders = null,
ext = null,
}: AddFileArgs): Promise<MiDriveFile> {
- const instance = await this.metaService.fetch();
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
-
const info = await this.fileInfoService.getFileInfo(path);
this.registerLogger.info(`${JSON.stringify(info)}`);
@@ -569,7 +564,7 @@ export class DriveService {
sensitive ?? false
: false;
- if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
+ if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (userRoleNSFW) file.isSensitive = true;
@@ -631,7 +626,7 @@ export class DriveService {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, true);
} else {
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true);
}
}
@@ -778,7 +773,7 @@ export class DriveService {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, false);
} else {
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false);
}
}
@@ -800,14 +795,13 @@ export class DriveService {
@bindThis
public async deleteObjectStorageFile(key: string) {
- const meta = await this.metaService.fetch();
try {
const param = {
- Bucket: meta.objectStorageBucket,
+ Bucket: this.meta.objectStorageBucket,
Key: key,
} as DeleteObjectCommandInput;
- await this.s3Service.delete(meta, param);
+ await this.s3Service.delete(this.meta, param);
} catch (err: any) {
if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 435dbbae28..a176474b95 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -5,18 +5,17 @@
import { URLSearchParams } from 'node:url';
import * as nodemailer from 'nodemailer';
+import juice from 'juice';
import { Inject, Injectable } from '@nestjs/common';
import { validate as validateEmail } from 'deep-email-validator';
-import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
-import type { UserProfilesRepository } from '@/models/_.js';
+import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
-import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -26,49 +25,41 @@ export class EmailService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- private metaService: MetaService,
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
- private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}
@bindThis
public async sendEmail(to: string, subject: string, html: string, text: string) {
- const meta = await this.metaService.fetch(true);
-
- if (!meta.enableEmail) return;
+ if (!this.meta.enableEmail) return;
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`;
- const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
+ const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
const transporter = nodemailer.createTransport({
- host: meta.smtpHost,
- port: meta.smtpPort,
- secure: meta.smtpSecure,
+ host: this.meta.smtpHost,
+ port: this.meta.smtpPort,
+ secure: this.meta.smtpSecure,
ignoreTLS: !enableAuth,
proxy: this.config.proxySmtp,
auth: enableAuth ? {
- user: meta.smtpUser,
- pass: meta.smtpPass,
+ user: this.meta.smtpUser,
+ pass: this.meta.smtpPass,
} : undefined,
} as any);
- try {
- // TODO: htmlサニタイズ
- const info = await transporter.sendMail({
- from: meta.email!,
- to: to,
- subject: subject,
- text: text,
- html: `<!doctype html>
+ const htmlContent = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
@@ -133,7 +124,7 @@ export class EmailService {
<body>
<main>
<header>
- <img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
+ <img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
</header>
<article>
<h1>${ subject }</h1>
@@ -147,7 +138,18 @@ export class EmailService {
<a href="${ this.config.url }">${ this.config.host }</a>
</nav>
</body>
-</html>`,
+</html>`;
+
+ const inlinedHtml = juice(htmlContent);
+
+ try {
+ // TODO: htmlサニタイズ
+ const info = await transporter.sendMail({
+ from: this.meta.email!,
+ to: to,
+ subject: subject,
+ text: text,
+ html: inlinedHtml,
});
this.logger.info(`Message sent: ${info.messageId}`);
@@ -162,8 +164,6 @@ export class EmailService {
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> {
- const meta = await this.metaService.fetch();
-
const exist = await this.userProfilesRepository.countBy({
emailVerified: true,
email: emailAddress,
@@ -181,11 +181,11 @@ export class EmailService {
reason?: string | null,
} = { valid: true, reason: null };
- if (meta.enableActiveEmailValidation) {
- if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
- validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
- } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
- validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
+ if (this.meta.enableActiveEmailValidation) {
+ if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
+ validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
+ } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
+ validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
} else {
validated = await validateEmail({
email: emailAddress,
@@ -215,7 +215,7 @@ export class EmailService {
}
const emailDomain: string = emailAddress.split('@')[1];
- const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
+ const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
if (isBanned) {
return {
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 753011cded..211c22bfaf 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -246,7 +246,7 @@ export interface InternalEventTypes {
avatarDecorationCreated: MiAvatarDecoration;
avatarDecorationDeleted: MiAvatarDecoration;
avatarDecorationUpdated: MiAvatarDecoration;
- metaUpdated: MiMeta;
+ metaUpdated: { before?: MiMeta; after: MiMeta; };
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
updateUserProfile: MiUserProfile;
diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts
index eb192ee6da..793bbeecb1 100644
--- a/packages/backend/src/core/HashtagService.ts
+++ b/packages/backend/src/core/HashtagService.ts
@@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { IdService } from '@/core/IdService.js';
import type { MiHashtag } from '@/models/Hashtag.js';
-import type { HashtagsRepository } from '@/models/_.js';
+import type { HashtagsRepository, MiMeta } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { FeaturedService } from '@/core/FeaturedService.js';
-import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
@Injectable()
export class HashtagService {
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.redis)
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
@@ -29,7 +31,6 @@ export class HashtagService {
private userEntityService: UserEntityService,
private featuredService: FeaturedService,
private idService: IdService,
- private metaService: MetaService,
private utilityService: UtilityService,
) {
}
@@ -160,10 +161,9 @@ export class HashtagService {
@bindThis
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
- const instance = await this.metaService.fetch();
- const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
+ const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
- if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
+ if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index ec630f804e..3d88d0aefe 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown {
switch (type) {
case 'metaUpdated': {
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
+ ...(body.after),
proxyAccount: null, // joinなカラムは通常取ってこないので
};
break;
@@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown {
});
}
- this.globalEventService.publishInternalEvent('metaUpdated', updated);
+ this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated });
return updated;
}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 94bb5af6b5..2055ea7f37 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -6,7 +6,7 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5';
-import { Window, DocumentFragment, XMLSerializer } from 'happy-dom';
+import { Window, XMLSerializer } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
@@ -465,7 +465,7 @@ export class MfmService {
const serialized = new XMLSerializer().serializeToString(body);
- happyDOM.close().catch(e => {});
+ happyDOM.close().catch(err => {});
return serialized;
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index ef0047ca90..17325d62b5 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -8,14 +8,13 @@ import * as mfm from '@transfem-org/sfm-js';
import { In, DataSource, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
-import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
import { LatestNote } from '@/models/LatestNote.js';
-import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -24,11 +23,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { IPoll } from '@/models/Poll.js';
import { MiPoll } from '@/models/Poll.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
-import { checkWordMute } from '@/misc/check-word-mute.js';
import type { MiChannel } from '@/models/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import { MemorySingleCache } from '@/misc/cache.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.js';
@@ -52,7 +48,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
-import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
@@ -64,6 +59,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -155,11 +151,15 @@ type Option = {
@Injectable()
export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
+ private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
constructor(
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.db)
private db: DataSource,
@@ -217,7 +217,6 @@ export class NoteCreateService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
private roleService: RoleService,
- private metaService: MetaService,
private searchService: SearchService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
@@ -226,7 +225,9 @@ export class NoteCreateService implements OnApplicationShutdown {
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
private cacheService: CacheService,
- ) { }
+ ) {
+ this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
+ }
@bindThis
public async create(user: {
@@ -259,10 +260,8 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true;
- const meta = await this.metaService.fetch();
-
if (data.visibility === 'public' && data.channel == null) {
- const sensitiveWords = meta.sensitiveWords;
+ const sensitiveWords = this.meta.sensitiveWords;
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
@@ -270,17 +269,17 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
- const hasProhibitedWords = await this.checkProhibitedWordsContain({
+ const hasProhibitedWords = this.checkProhibitedWordsContain({
cw: data.cw,
text: data.text,
pollChoices: data.poll?.choices,
- }, meta.prohibitedWords);
+ }, this.meta.prohibitedWords);
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
- const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
+ const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
data.visibility = 'home';
@@ -385,7 +384,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// if the host is media-silenced, custom emojis are not allowed
- if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
+ if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
@@ -556,10 +555,8 @@ export class NoteCreateService implements OnApplicationShutdown {
isBot: MiUser['isBot'];
noindex: MiUser['noindex'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
- const meta = await this.metaService.fetch();
-
this.notesChart.update(note, true);
- if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
+ if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
this.perUserNotesChart.update(user, note, true);
}
@@ -567,11 +564,11 @@ export class NoteCreateService implements OnApplicationShutdown {
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => {
if (note.renote && note.text) {
- this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
+ this.updateNotesCountQueue.enqueue(i.id, 1);
} else if (!note.renote) {
- this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
+ this.updateNotesCountQueue.enqueue(i.id, 1);
}
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true);
}
});
@@ -953,15 +950,14 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
- const meta = await this.metaService.fetch();
- if (!meta.enableFanoutTimeline) return;
+ if (!this.meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
- this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
@@ -971,9 +967,9 @@ export class NoteCreateService implements OnApplicationShutdown {
});
for (const channelFollowing of channelFollowings) {
- this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
@@ -1011,9 +1007,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!following.withReplies) continue;
}
- this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
@@ -1030,25 +1026,25 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue;
}
- this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
}
}
// 自分自身のHTL
if (note.userHost == null) {
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
- this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
}
// 自分自身以外への返信
if (isReply(note)) {
- this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
@@ -1057,9 +1053,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
} else {
- this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
@@ -1118,9 +1114,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
- public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
+ public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
if (prohibitedWords == null) {
- prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
+ prohibitedWords = this.meta.prohibitedWords;
}
if (
@@ -1136,13 +1132,24 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- public dispose(): void {
+ private collapseNotesCount(oldValue: number, newValue: number) {
+ return oldValue + newValue;
+ }
+
+ @bindThis
+ private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
+ await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
+ }
+
+ @bindThis
+ public async dispose(): Promise<void> {
this.#shutdownController.abort();
+ await this.updateNotesCountQueue.performAllNow();
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
+ public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
+ await this.dispose();
}
private async updateLatestNote(note: MiNote) {
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index 3f86f41942..6ea400b03e 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -8,7 +8,7 @@ import { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
import { LatestNote } from '@/models/LatestNote.js';
-import type { InstancesRepository, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
+import type { InstancesRepository, MiMeta, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.js';
@@ -20,9 +20,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
@@ -33,6 +31,9 @@ export class NoteDeleteService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -46,13 +47,11 @@ export class NoteDeleteService {
private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService,
- private noteEntityService: NoteEntityService,
private globalEventService: GlobalEventService,
private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
- private metaService: MetaService,
private searchService: SearchService,
private moderationLogService: ModerationLogService,
private notesChart: NotesChart,
@@ -113,10 +112,8 @@ export class NoteDeleteService {
}
//#endregion
- const meta = await this.metaService.fetch();
-
this.notesChart.update(note, false);
- if (meta.enableChartsForRemoteUser || (user.host == null)) {
+ if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false);
}
@@ -135,7 +132,7 @@ export class NoteDeleteService {
} else if (!note.renoteId) {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
}
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false);
}
});
diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts
index 71d663bf90..c3ff2a68d3 100644
--- a/packages/backend/src/core/ProxyAccountService.ts
+++ b/packages/backend/src/core/ProxyAccountService.ts
@@ -4,26 +4,25 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ProxyAccountService {
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
-
- private metaService: MetaService,
) {
}
@bindThis
public async fetch(): Promise<MiLocalUser | null> {
- const meta = await this.metaService.fetch();
- if (meta.proxyAccountId == null) return null;
- return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
+ if (this.meta.proxyAccountId == null) return null;
+ return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
}
}
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 6a845b951d..1479bb00d9 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
-import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RedisKVCache } from '@/misc/cache.js';
@@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
-
- private metaService: MetaService,
) {
this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
lifetime: 1000 * 60 * 60 * 1, // 1h
@@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown {
@bindThis
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
- const meta = await this.metaService.fetch();
-
- if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
+ if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(this.config.url,
- meta.swPublicKey,
- meta.swPrivateKey);
+ this.meta.swPublicKey,
+ this.meta.swPrivateKey);
const subscriptions = await this.subscriptionsCache.fetch(userId);
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index be5f10771a..dc13aa21bf 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -88,6 +88,12 @@ export class QueueService {
repeat: { pattern: '*/5 * * * *' },
removeOnComplete: true,
});
+
+ this.systemQueue.add('bakeBufferedReactions', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
}
@bindThis
@@ -511,10 +517,15 @@ export class QueueService {
/**
* @see UserWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see UserWebhookDeliverProcessorService
*/
@bindThis
- public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ public userWebhookDeliver(
+ webhook: MiWebhook,
+ type: typeof webhookEventTypes[number],
+ content: unknown,
+ opts?: { attempts?: number },
+ ) {
const data: UserWebhookDeliverJobData = {
type,
content,
@@ -527,7 +538,7 @@ export class QueueService {
};
return this.userWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
@@ -538,10 +549,15 @@ export class QueueService {
/**
* @see SystemWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see SystemWebhookDeliverProcessorService
*/
@bindThis
- public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ public systemWebhookDeliver(
+ webhook: MiSystemWebhook,
+ type: SystemWebhookEventType,
+ content: unknown,
+ opts?: { attempts?: number },
+ ) {
const data: SystemWebhookDeliverJobData = {
type,
content,
@@ -553,7 +569,7 @@ export class QueueService {
};
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index 17ff168786..0179b0680f 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -4,9 +4,8 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
-import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js';
+import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository, MiMeta } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
@@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
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 '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
@@ -30,9 +28,10 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
const FALLBACK = '\u2764';
-const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = {
'like': '👍',
@@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@([\w.
@Injectable()
export class ReactionService {
constructor(
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
+ @Inject(DI.meta)
+ private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -90,12 +89,12 @@ export class ReactionService {
private emojisRepository: EmojisRepository,
private utilityService: UtilityService,
- private metaService: MetaService,
private customEmojiService: CustomEmojiService,
private roleService: RoleService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
+ private reactionsBufferingService: ReactionsBufferingService,
private idService: IdService,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
@@ -108,8 +107,6 @@ export class ReactionService {
@bindThis
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
- const meta = await this.metaService.fetch();
-
// Check blocking
if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -155,7 +152,7 @@ export class ReactionService {
}
// for media silenced host, custom emoji reactions are not allowed
- if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
+ if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
reaction = FALLBACK;
}
} else {
@@ -177,7 +174,6 @@ export class ReactionService {
reaction,
};
- // Create reaction
try {
await this.noteReactionsRepository.insert(record);
} catch (e) {
@@ -201,16 +197,20 @@ export class ReactionService {
}
// Increment reactions count
- const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
- await this.notesRepository.createQueryBuilder().update()
- .set({
- reactions: () => sql,
- ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
- reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
- } : {}),
- })
- .where('id = :id', { id: note.id })
- .execute();
+ if (this.meta.enableReactionsBuffering) {
+ await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
+ } else {
+ const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+ await this.notesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
+ reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
+ } : {}),
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ }
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
if (
@@ -230,7 +230,7 @@ export class ReactionService {
}
}
- if (meta.enableChartsForRemoteUser || (user.host == null)) {
+ if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}
@@ -317,14 +317,18 @@ export class ReactionService {
}
// Decrement reactions count
- const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
- await this.notesRepository.createQueryBuilder().update()
- .set({
- reactions: () => sql,
- reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
- })
- .where('id = :id', { id: note.id })
- .execute();
+ if (this.meta.enableReactionsBuffering) {
+ await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
+ } else {
+ const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+ await this.notesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ }
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
@@ -346,8 +350,21 @@ export class ReactionService {
}
/**
- * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
- * データベース上には存在する「0個のリアクションがついている」という情報を削除する。
+ * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
+ * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
+ */
+ @bindThis
+ public convertLegacyReaction(reaction: string): string {
+ reaction = this.decodeReaction(reaction).reaction;
+ if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
+ return reaction;
+ }
+
+ // TODO: 廃止
+ /**
+ * - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
+ * - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
+ * - データベース上には存在する「0個のリアクションがついている」という情報を削除する
*/
@bindThis
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
@@ -360,10 +377,7 @@ export class ReactionService {
return count > 0;
})
.map(([reaction, count]) => {
- // unchecked indexed access
- const convertedReaction = legacies[reaction] as string | undefined;
-
- const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
+ const key = this.convertLegacyReaction(reaction);
return [key, count] as const;
})
@@ -418,11 +432,4 @@ export class ReactionService {
host: undefined,
};
}
-
- @bindThis
- public convertLegacyReaction(reaction: string): string {
- reaction = this.decodeReaction(reaction).reaction;
- if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
- return reaction;
- }
}
diff --git a/packages/backend/src/core/ReactionsBufferingService.ts b/packages/backend/src/core/ReactionsBufferingService.ts
new file mode 100644
index 0000000000..b4207c5106
--- /dev/null
+++ b/packages/backend/src/core/ReactionsBufferingService.ts
@@ -0,0 +1,211 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DI } from '@/di-symbols.js';
+import type { MiNote } from '@/models/Note.js';
+import { bindThis } from '@/decorators.js';
+import type { MiUser, NotesRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
+const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
+
+@Injectable()
+export class ReactionsBufferingService implements OnApplicationShutdown {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+
+ @Inject(DI.redisForReactions)
+ private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string) {
+ const obj = JSON.parse(data);
+
+ if (obj.channel === 'internal') {
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'metaUpdated': {
+ // リアクションバッファリングが有効→無効になったら即bake
+ if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) {
+ this.bake();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
+ for (let i = 0; i < currentPairs.length; i++) {
+ pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
+ }
+ pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
+ pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
+ await pipeline.exec();
+ }
+
+ @bindThis
+ public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
+ pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
+ // TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
+ await pipeline.exec();
+ }
+
+ @bindThis
+ public async get(noteId: MiNote['id']): Promise<{
+ deltas: Record<string, number>;
+ pairs: ([MiUser['id'], string])[];
+ }> {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
+ pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
+ const results = await pipeline.exec();
+
+ const resultDeltas = results![0][1] as Record<string, string>;
+ const resultPairs = results![1][1] as string[];
+
+ const deltas = {} as Record<string, number>;
+ for (const [name, count] of Object.entries(resultDeltas)) {
+ deltas[name] = parseInt(count);
+ }
+
+ const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
+
+ return {
+ deltas,
+ pairs,
+ };
+ }
+
+ @bindThis
+ public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
+ deltas: Record<string, number>;
+ pairs: ([MiUser['id'], string])[];
+ }>> {
+ const map = new Map<MiNote['id'], {
+ deltas: Record<string, number>;
+ pairs: ([MiUser['id'], string])[];
+ }>();
+
+ const pipeline = this.redisForReactions.pipeline();
+ for (const noteId of noteIds) {
+ pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
+ pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
+ }
+ const results = await pipeline.exec();
+
+ const opsForEachNotes = 2;
+ for (let i = 0; i < noteIds.length; i++) {
+ const noteId = noteIds[i];
+ const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
+ const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
+
+ const deltas = {} as Record<string, number>;
+ for (const [name, count] of Object.entries(resultDeltas)) {
+ deltas[name] = parseInt(count);
+ }
+
+ const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
+
+ map.set(noteId, {
+ deltas,
+ pairs,
+ });
+ }
+
+ return map;
+ }
+
+ // TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
+ @bindThis
+ public async bake(): Promise<void> {
+ const bufferedNoteIds = [];
+ let cursor = '0';
+ do {
+ // https://github.com/redis/ioredis#transparent-key-prefixing
+ const result = await this.redisForReactions.scan(
+ cursor,
+ 'MATCH',
+ `${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
+ 'COUNT',
+ '1000');
+
+ cursor = result[0];
+ bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
+ } while (cursor !== '0');
+
+ const bufferedMap = await this.getMany(bufferedNoteIds);
+
+ // clear
+ const pipeline = this.redisForReactions.pipeline();
+ for (const noteId of bufferedNoteIds) {
+ pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
+ pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
+ }
+ await pipeline.exec();
+
+ // TODO: SQL一個にまとめたい
+ for (const [noteId, buffered] of bufferedMap) {
+ const sql = Object.entries(buffered.deltas)
+ .map(([reaction, count]) =>
+ `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
+ .join(' || ');
+
+ this.notesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
+ })
+ .where('id = :id', { id: noteId })
+ .execute();
+ }
+ }
+
+ @bindThis
+ public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] {
+ const reactions = { ...src };
+ for (const [name, count] of Object.entries(delta)) {
+ if (reactions[name] != null) {
+ reactions[name] += count;
+ } else {
+ reactions[name] = count;
+ }
+ }
+ return reactions;
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 7984dc5627..64f7539031 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import type {
+ MiMeta,
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
@@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -60,6 +60,11 @@ export type RolePolicies = {
rateLimitFactor: number;
canImportNotes: boolean;
avatarDecorationLimit: number;
+ canImportAntennas: boolean;
+ canImportBlocking: boolean;
+ canImportFollowing: boolean;
+ canImportMuting: boolean;
+ canImportUserLists: boolean;
};
export const DEFAULT_POLICIES: RolePolicies = {
@@ -91,6 +96,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
rateLimitFactor: 1,
canImportNotes: true,
avatarDecorationLimit: 1,
+ canImportAntennas: true,
+ canImportBlocking: true,
+ canImportFollowing: true,
+ canImportMuting: true,
+ canImportUserLists: true,
};
@Injectable()
@@ -105,8 +115,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
constructor(
private moduleRef: ModuleRef,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
+ @Inject(DI.meta)
+ private meta: MiMeta,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@@ -123,7 +133,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
- private metaService: MetaService,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
@@ -343,8 +352,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
- const meta = await this.metaService.fetch();
- const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
+ const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
if (userId == null) return basePolicies;
@@ -393,6 +401,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
+ canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
+ canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
+ canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
+ canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
+ canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
};
}
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 80907a8921..ce23ffd626 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -9,7 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
import { DataSource, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
@@ -21,7 +21,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
-import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js';
@Injectable()
@@ -30,6 +29,9 @@ export class SignupService {
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -40,7 +42,6 @@ export class SignupService {
private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
- private metaService: MetaService,
private instanceActorService: InstanceActorService,
private usersChart: UsersChart,
) {
@@ -91,7 +92,7 @@ export class SignupService {
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
- const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+ const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new Error('USED_USERNAME');
}
@@ -163,4 +164,3 @@ export class SignupService {
return { account, secret };
}
}
-
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index bc6851f788..bb7c6b8c0e 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
* SystemWebhook の一覧を取得する.
*/
@bindThis
- public async fetchSystemWebhooks(params?: {
+ public fetchSystemWebhooks(params?: {
ids?: MiSystemWebhook['id'][];
isActive?: MiSystemWebhook['isActive'];
on?: MiSystemWebhook['on'];
@@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
+ * // TODO: contentの型を厳格化する
*/
@bindThis
- public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
+ webhook: MiSystemWebhook | MiSystemWebhook['id'],
+ type: T,
+ content: unknown,
+ ) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
: webhook;
if (!webhookEntity || !webhookEntity.isActive) {
- this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
return;
}
if (!webhookEntity.on.includes(type)) {
- this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
return;
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 6aab8fde70..77e7b60bea 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
-import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.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 { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js';
-import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js';
@@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
- private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
- private fanoutTimelineService: FanoutTimelineService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
followee.isLocked ||
(followeeProfile.carefulBot && follower.isBot) ||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
- (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
+ (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
) {
let autoAccept = false;
@@ -277,16 +275,19 @@ export class UserFollowingService implements OnModuleInit {
followeeId: followee.id,
followerId: follower.id,
});
-
- // 通知を作成
- if (follower.host === null) {
- this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
- }, followee.id);
- }
}
if (alreadyFollowed) return;
+ // 通知を作成
+ if (follower.host === null) {
+ const profile = await this.cacheService.userProfileCache.fetch(followee.id);
+
+ this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
+ message: profile.followedMessage,
+ }, followee.id);
+ }
+
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
const [followeeUser, followerUser] = await Promise.all([
@@ -307,14 +308,14 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true);
}
});
@@ -439,14 +440,14 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index e96bfeea95..8a40a53688 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -5,8 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
+import { type WebhooksRepository } from '@/models/_.js';
+import { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
return this.activeWebhooks;
}
+ /**
+ * UserWebhook の一覧を取得する.
+ */
+ @bindThis
+ public fetchWebhooks(params?: {
+ ids?: MiWebhook['id'][];
+ isActive?: MiWebhook['active'];
+ on?: MiWebhook['on'];
+ }): Promise<MiWebhook[]> {
+ const query = this.webhooksRepository.createQueryBuilder('webhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ webhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 22871e44f8..009dd4665f 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -10,12 +10,16 @@ import RE2 from 're2';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
+import { MiMeta } from '@/models/Meta.js';
@Injectable()
export class UtilityService {
constructor(
@Inject(DI.config)
private config: Config,
+
+ @Inject(DI.meta)
+ private meta: MiMeta,
) {
}
@@ -112,4 +116,18 @@ export class UtilityService {
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
+
+ public isFederationAllowedHost(host: string): boolean {
+ if (this.meta.federation === 'none') return false;
+ if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
+ if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
+
+ return true;
+ }
+
+ @bindThis
+ public isFederationAllowedUri(uri: string): boolean {
+ const host = this.extractDbHost(uri);
+ return this.isFederationAllowedHost(host);
+ }
}
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index ec9f4484a4..75ab0a207c 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -12,10 +12,9 @@ import {
} from '@simplewebauthn/server';
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
import { DI } from '@/di-symbols.js';
-import type { UserSecurityKeysRepository } from '@/models/_.js';
+import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiUser } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type {
@@ -23,7 +22,6 @@ import type {
AuthenticatorTransportFuture,
CredentialDeviceType,
PublicKeyCredentialCreationOptionsJSON,
- PublicKeyCredentialDescriptorFuture,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/types';
@@ -31,33 +29,33 @@ import type {
@Injectable()
export class WebAuthnService {
constructor(
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
-
- private metaService: MetaService,
) {
}
@bindThis
- public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
- const instance = await this.metaService.fetch();
+ public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
return {
origin: this.config.url,
rpId: this.config.hostname,
- rpName: instance.name ?? this.config.host,
- rpIcon: instance.iconUrl ?? undefined,
+ rpName: this.meta.name ?? this.config.host,
+ rpIcon: this.meta.iconUrl ?? undefined,
};
}
@bindThis
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
- const relyingParty = await this.getRelyingParty();
+ const relyingParty = this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
@@ -104,7 +102,7 @@ export class WebAuthnService {
await this.redisClient.del(`webauthn:challenge:${userId}`);
- const relyingParty = await this.getRelyingParty();
+ const relyingParty = this.getRelyingParty();
let verification;
try {
@@ -143,7 +141,7 @@ export class WebAuthnService {
@bindThis
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
- const relyingParty = await this.getRelyingParty();
+ const relyingParty = this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
@@ -166,6 +164,86 @@ export class WebAuthnService {
return authenticationOptions;
}
+ /**
+ * Initiate Passkey Auth (Without specifying user)
+ * @returns authenticationOptions
+ */
+ @bindThis
+ public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
+ const relyingParty = await this.getRelyingParty();
+
+ const authenticationOptions = await generateAuthenticationOptions({
+ rpID: relyingParty.rpId,
+ userVerification: 'preferred',
+ });
+
+ await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);
+
+ return authenticationOptions;
+ }
+
+ /**
+ * Verify Webauthn AuthenticationCredential
+ * @throws IdentifiableError
+ * @returns If the challenge is successful, return the user ID. Otherwise, return null.
+ */
+ @bindThis
+ public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
+ const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
+
+ if (!challenge) {
+ throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
+ }
+
+ await this.redisClient.del(`webauthn:challenge:${context}`);
+
+ const key = await this.userSecurityKeysRepository.findOneBy({
+ id: response.id,
+ });
+
+ if (!key) {
+ throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
+ }
+
+ const relyingParty = await this.getRelyingParty();
+
+ let verification;
+ try {
+ verification = await verifyAuthenticationResponse({
+ response: response,
+ expectedChallenge: challenge,
+ expectedOrigin: relyingParty.origin,
+ expectedRPID: relyingParty.rpId,
+ authenticator: {
+ credentialID: key.id,
+ credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
+ counter: key.counter,
+ transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
+ },
+ requireUserVerification: true,
+ });
+ } catch (error) {
+ throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
+ }
+
+ const { verified, authenticationInfo } = verification;
+
+ if (!verified) {
+ return null;
+ }
+
+ await this.userSecurityKeysRepository.update({
+ id: response.id,
+ }, {
+ lastUsed: new Date(),
+ counter: authenticationInfo.newCounter,
+ credentialDeviceType: authenticationInfo.credentialDeviceType,
+ credentialBackedUp: authenticationInfo.credentialBackedUp,
+ });
+
+ return key.userId;
+ }
+
@bindThis
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
@@ -209,7 +287,7 @@ export class WebAuthnService {
}
}
- const relyingParty = await this.getRelyingParty();
+ const relyingParty = this.getRelyingParty();
let verification;
try {
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
new file mode 100644
index 0000000000..c2764f30e8
--- /dev/null
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -0,0 +1,435 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { type WebhookEventTypes } from '@/models/Webhook.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { QueueService } from '@/core/QueueService.js';
+
+const oneDayMillis = 24 * 60 * 60 * 1000;
+
+function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
+ return {
+ id: 'dummy-abuse-report1',
+ targetUserId: 'dummy-target-user',
+ targetUser: null,
+ reporterId: 'dummy-reporter-user',
+ reporter: null,
+ assigneeId: null,
+ assignee: null,
+ resolved: false,
+ forwarded: false,
+ comment: 'This is a dummy report for testing purposes.',
+ targetUserHost: null,
+ reporterHost: null,
+ ...override,
+ };
+}
+
+function generateDummyUser(override?: Partial<MiUser>): MiUser {
+ return {
+ id: 'dummy-user-1',
+ updatedAt: new Date(Date.now() - oneDayMillis * 7),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
+ lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
+ hideOnlineStatus: false,
+ username: 'dummy1',
+ usernameLower: 'dummy1',
+ name: 'DummyUser1',
+ followersCount: 10,
+ followingCount: 5,
+ movedToUri: null,
+ movedAt: null,
+ alsoKnownAs: null,
+ notesCount: 30,
+ avatarId: null,
+ avatar: null,
+ bannerId: null,
+ banner: null,
+ avatarUrl: null,
+ bannerUrl: null,
+ avatarBlurhash: null,
+ bannerBlurhash: null,
+ avatarDecorations: [],
+ tags: [],
+ isSuspended: false,
+ isLocked: false,
+ isBot: false,
+ isCat: true,
+ isRoot: false,
+ isExplorable: true,
+ isHibernated: false,
+ isDeleted: false,
+ emojis: [],
+ score: 0,
+ host: null,
+ inbox: null,
+ sharedInbox: null,
+ featured: null,
+ uri: null,
+ followersUri: null,
+ token: null,
+ ...override,
+ };
+}
+
+function generateDummyNote(override?: Partial<MiNote>): MiNote {
+ return {
+ id: 'dummy-note-1',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: 'This is a dummy note for testing purposes.',
+ name: null,
+ cw: null,
+ userId: 'dummy-user-1',
+ user: null,
+ localOnly: true,
+ reactionAcceptance: 'likeOnly',
+ renoteCount: 10,
+ repliesCount: 5,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '[]',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+ ...override,
+ };
+}
+
+function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
+ return {
+ id: note.id,
+ createdAt: new Date().toISOString(),
+ deletedAt: null,
+ text: note.text,
+ cw: note.cw,
+ userId: note.userId,
+ user: toPackedUserLite(note.user ?? generateDummyUser()),
+ replyId: note.replyId,
+ renoteId: note.renoteId,
+ isHidden: false,
+ visibility: note.visibility,
+ mentions: note.mentions,
+ visibleUserIds: note.visibleUserIds,
+ fileIds: note.fileIds,
+ files: [],
+ tags: note.tags,
+ poll: null,
+ emojis: note.emojis,
+ channelId: note.channelId,
+ channel: note.channel,
+ localOnly: note.localOnly,
+ reactionAcceptance: note.reactionAcceptance,
+ reactionEmojis: {},
+ reactions: {},
+ reactionCount: 0,
+ renoteCount: note.renoteCount,
+ repliesCount: note.repliesCount,
+ uri: note.uri ?? undefined,
+ url: note.url ?? undefined,
+ reactionAndUserPairCache: note.reactionAndUserPairCache,
+ ...(detail ? {
+ clippedCount: note.clippedCount,
+ reply: note.reply ? toPackedNote(note.reply, false) : null,
+ renote: note.renote ? toPackedNote(note.renote, true) : null,
+ myReaction: null,
+ } : {}),
+ ...override,
+ };
+}
+
+function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
+ return {
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ host: user.host,
+ avatarUrl: user.avatarUrl,
+ avatarBlurhash: user.avatarBlurhash,
+ avatarDecorations: user.avatarDecorations.map(it => ({
+ id: it.id,
+ angle: it.angle,
+ flipH: it.flipH,
+ url: 'https://example.com/dummy-image001.png',
+ offsetX: it.offsetX,
+ offsetY: it.offsetY,
+ })),
+ isBot: user.isBot,
+ isCat: user.isCat,
+ emojis: user.emojis,
+ onlineStatus: 'active',
+ badgeRoles: [],
+ ...override,
+ };
+}
+
+function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
+ return {
+ ...toPackedUserLite(user),
+ url: null,
+ uri: null,
+ movedTo: null,
+ alsoKnownAs: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: user.updatedAt?.toISOString() ?? null,
+ lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
+ bannerUrl: user.bannerUrl,
+ bannerBlurhash: user.bannerBlurhash,
+ isLocked: user.isLocked,
+ isSilenced: false,
+ isSuspended: user.isSuspended,
+ description: null,
+ location: null,
+ birthday: null,
+ lang: null,
+ fields: [],
+ verifiedLinks: [],
+ followersCount: user.followersCount,
+ followingCount: user.followingCount,
+ notesCount: user.notesCount,
+ pinnedNoteIds: [],
+ pinnedNotes: [],
+ pinnedPageId: null,
+ pinnedPage: null,
+ publicReactions: true,
+ followersVisibility: 'public',
+ followingVisibility: 'public',
+ twoFactorEnabled: false,
+ usePasswordLessLogin: false,
+ securityKeys: false,
+ roles: [],
+ memo: null,
+ moderationNote: undefined,
+ isFollowing: false,
+ isFollowed: false,
+ hasPendingFollowRequestFromYou: false,
+ hasPendingFollowRequestToYou: false,
+ isBlocking: false,
+ isBlocked: false,
+ isMuted: false,
+ isRenoteMuted: false,
+ notify: 'none',
+ withReplies: true,
+ ...override,
+ };
+}
+
+const dummyUser1 = generateDummyUser();
+const dummyUser2 = generateDummyUser({
+ id: 'dummy-user-2',
+ updatedAt: new Date(Date.now() - oneDayMillis * 30),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis),
+ lastActiveDate: new Date(Date.now() - oneDayMillis),
+ username: 'dummy2',
+ usernameLower: 'dummy2',
+ name: 'DummyUser2',
+ followersCount: 40,
+ followingCount: 50,
+ notesCount: 900,
+});
+const dummyUser3 = generateDummyUser({
+ id: 'dummy-user-3',
+ updatedAt: new Date(Date.now() - oneDayMillis * 15),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
+ lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
+ username: 'dummy3',
+ usernameLower: 'dummy3',
+ name: 'DummyUser3',
+ followersCount: 60,
+ followingCount: 70,
+ notesCount: 15900,
+});
+
+@Injectable()
+export class WebhookTestService {
+ public static NoSuchWebhookError = class extends Error {};
+
+ constructor(
+ private userWebhookService: UserWebhookService,
+ private systemWebhookService: SystemWebhookService,
+ private queueService: QueueService,
+ ) {
+ }
+
+ /**
+ * UserWebhookのテスト送信を行う.
+ * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+ *
+ * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+ * - Webhookそのものの有効・無効設定(active)
+ * - 送信対象イベント(on)に関する設定
+ */
+ @bindThis
+ public async testUserWebhook(
+ params: {
+ webhookId: MiWebhook['id'],
+ type: WebhookEventTypes,
+ override?: Partial<Omit<MiWebhook, 'id'>>,
+ },
+ sender: MiUser | null,
+ ) {
+ const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
+ .then(it => it.filter(it => it.userId === sender?.id));
+ if (webhooks.length === 0) {
+ throw new WebhookTestService.NoSuchWebhookError();
+ }
+
+ const webhook = webhooks[0];
+ const send = (contents: unknown) => {
+ const merged = {
+ ...webhook,
+ ...params.override,
+ };
+
+ // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+ // また、Jobの試行回数も1回だけ.
+ this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ };
+
+ const dummyNote1 = generateDummyNote({
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ });
+ const dummyReply1 = generateDummyNote({
+ id: 'dummy-reply-1',
+ replyId: dummyNote1.id,
+ reply: dummyNote1,
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ });
+ const dummyRenote1 = generateDummyNote({
+ id: 'dummy-renote-1',
+ renoteId: dummyNote1.id,
+ renote: dummyNote1,
+ userId: dummyUser2.id,
+ user: dummyUser2,
+ text: null,
+ });
+ const dummyMention1 = generateDummyNote({
+ id: 'dummy-mention-1',
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ text: `@${dummyUser2.username} This is a mention to you.`,
+ mentions: [dummyUser2.id],
+ });
+
+ switch (params.type) {
+ case 'note': {
+ send(toPackedNote(dummyNote1));
+ break;
+ }
+ case 'reply': {
+ send(toPackedNote(dummyReply1));
+ break;
+ }
+ case 'renote': {
+ send(toPackedNote(dummyRenote1));
+ break;
+ }
+ case 'mention': {
+ send(toPackedNote(dummyMention1));
+ break;
+ }
+ case 'follow': {
+ send(toPackedUserDetailedNotMe(dummyUser1));
+ break;
+ }
+ case 'followed': {
+ send(toPackedUserLite(dummyUser2));
+ break;
+ }
+ case 'unfollow': {
+ send(toPackedUserDetailedNotMe(dummyUser3));
+ break;
+ }
+ }
+ }
+
+ /**
+ * SystemWebhookのテスト送信を行う.
+ * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+ *
+ * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+ * - Webhookそのものの有効・無効設定(isActive)
+ * - 送信対象イベント(on)に関する設定
+ */
+ @bindThis
+ public async testSystemWebhook(
+ params: {
+ webhookId: MiSystemWebhook['id'],
+ type: SystemWebhookEventType,
+ override?: Partial<Omit<MiSystemWebhook, 'id'>>,
+ },
+ ) {
+ const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
+ if (webhooks.length === 0) {
+ throw new WebhookTestService.NoSuchWebhookError();
+ }
+
+ const webhook = webhooks[0];
+ const send = (contents: unknown) => {
+ const merged = {
+ ...webhook,
+ ...params.override,
+ };
+
+ // テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+ // また、Jobの試行回数も1回だけ.
+ this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ };
+
+ switch (params.type) {
+ case 'abuseReport': {
+ send(generateAbuseReport({
+ targetUserId: dummyUser1.id,
+ targetUser: dummyUser1,
+ reporterId: dummyUser2.id,
+ reporter: dummyUser2,
+ }));
+ break;
+ }
+ case 'abuseReportResolved': {
+ send(generateAbuseReport({
+ targetUserId: dummyUser1.id,
+ targetUser: dummyUser1,
+ reporterId: dummyUser2.id,
+ reporter: dummyUser2,
+ assigneeId: dummyUser3.id,
+ assignee: dummyUser3,
+ resolved: true,
+ }));
+ break;
+ }
+ case 'userCreated': {
+ send(toPackedUserLite(dummyUser1));
+ break;
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index bce67a458f..9f300e9905 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -18,14 +18,13 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
import { IdService } from '@/core/IdService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
-import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@@ -50,6 +49,9 @@ export class ApInboxService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -66,7 +68,6 @@ export class ApInboxService {
private noteEntityService: NoteEntityService,
private utilityService: UtilityService,
private idService: IdService,
- private metaService: MetaService,
private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
@@ -292,9 +293,8 @@ export class ApInboxService {
return;
}
- // アナウンス先をブロックしてたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
+ // アナウンス先が許可されているかチェック
+ if (!this.utilityService.isFederationAllowedUri(uri)) return;
const unlock = await this.appLockService.getApLock(uri);
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 499a163d6c..a5feb8b30c 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -517,6 +517,7 @@ export class ApRendererService {
name: user.name,
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description,
+ _misskey_followedMessage: profile.followedMessage,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
backgroundUrl: background ? this.renderImage(background) : null,
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 0b9139db90..38c78cf900 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -208,12 +208,12 @@ export class ApRequestService {
const contentType = res.headers.get('content-type');
if (
- res.ok
- && (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html'
- && _followAlternate === true
+ res.ok &&
+ (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' &&
+ _followAlternate === true
) {
const html = await res.text();
- const window = new Window({
+ const { window, happyDOM } = new Window({
settings: {
disableJavaScriptEvaluation: true,
disableJavaScriptFileLoading: true,
@@ -247,7 +247,7 @@ export class ApRequestService {
} catch (e) {
// something went wrong parsing the HTML, ignore the whole thing
} finally {
- await window.happyDOM.close();
+ happyDOM.close().catch(err => {});
}
}
//#endregion
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index b047a6c59b..f9411a1283 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
-import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
+import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -29,6 +28,7 @@ export class Resolver {
constructor(
private config: Config,
+ private meta: MiMeta,
private usersRepository: UsersRepository,
private notesRepository: NotesRepository,
private pollsRepository: PollsRepository,
@@ -36,7 +36,6 @@ export class Resolver {
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService,
private instanceActorService: InstanceActorService,
- private metaService: MetaService,
private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService,
@@ -94,8 +93,7 @@ export class Resolver {
return await this.resolveLocal(value);
}
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
+ if (!this.utilityService.isFederationAllowedHost(host)) {
throw new Error('Instance is blocked');
}
@@ -186,6 +184,9 @@ export class ApResolverService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -203,7 +204,6 @@ export class ApResolverService {
private utilityService: UtilityService,
private instanceActorService: InstanceActorService,
- private metaService: MetaService,
private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService,
@@ -216,6 +216,7 @@ export class ApResolverService {
public createResolver(): Resolver {
return new Resolver(
this.config,
+ this.meta,
this.usersRepository,
this.notesRepository,
this.pollsRepository,
@@ -223,7 +224,6 @@ export class ApResolverService {
this.followRequestsRepository,
this.utilityService,
this.instanceActorService,
- this.metaService,
this.apRequestService,
this.httpRequestService,
this.apRendererService,
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index 86a665732a..da75fc1d42 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -557,6 +557,7 @@ const extension_context_definition = {
'_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary',
+ '_misskey_followedMessage': 'misskey:_misskey_followedMessage',
'isCat': 'misskey:isCat',
// Firefish
firefish: 'https://joinfirefish.org/ns#',
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index b281ac9728..ba9f41ca24 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import { MetaService } from '@/core/MetaService.js';
import { truncate } from '@/misc/truncate.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js';
@@ -25,10 +24,12 @@ export class ApImageService {
private logger: Logger;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- private metaService: MetaService,
private apResolverService: ApResolverService,
private driveService: DriveService,
private apLoggerService: ApLoggerService,
@@ -65,12 +66,10 @@ export class ApImageService {
this.logger.info(`Creating the Image: ${image.url}`);
- const instance = await this.metaService.fetch();
-
// Cache if remote file cache is on AND either
// 1. remote sensitive file is also on
// 2. or the image is not sensitive
- const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
+ const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
await this.federatedInstanceService.fetch(actor.host).then(async i => {
if (i.isNSFW) {
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 382cda301f..edc9946c07 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -6,13 +6,12 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js';
+import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js';
-import { MetaService } from '@/core/MetaService.js';
import { AppLockService } from '@/core/AppLockService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
@@ -47,6 +46,9 @@ export class ApNoteService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -69,7 +71,6 @@ export class ApNoteService {
private apMentionService: ApMentionService,
private apImageService: ApImageService,
private apQuestionService: ApQuestionService,
- private metaService: MetaService,
private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
@@ -187,7 +188,7 @@ export class ApNoteService {
/**
* 禁止ワードチェック
*/
- const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
+ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
@@ -565,9 +566,7 @@ export class ApNoteService {
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
const uri = getApId(value);
- // ブロックしていたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
throw new StatusError('blocked host', 451);
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 10b1fc0bf4..2046dad099 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
+import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { MiUser } from '@/models/User.js';
@@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
-import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js';
@@ -46,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js';
import type { ApMfmService } from '../ApMfmService.js';
import type { ApResolverService, Resolver } from '../ApResolverService.js';
import type { ApLoggerService } from '../ApLoggerService.js';
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+
import type { ApImageService } from './ApImageService.js';
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
@@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService;
private idService: IdService;
private globalEventService: GlobalEventService;
- private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService;
private cacheService: CacheService;
@@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.db)
private db: DataSource,
@@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit {
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService');
- this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.cacheService = this.moduleRef.get('CacheService');
@@ -319,8 +319,8 @@ export class ApPersonService implements OnModuleInit {
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
}
return 'private';
- })
- )
+ }),
+ ),
);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -395,6 +395,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new MiUserProfile({
userId: user.id,
description: _description,
+ followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
url,
fields,
followingVisibility,
@@ -433,10 +434,10 @@ export class ApPersonService implements OnModuleInit {
this.cacheService.uriPersonCache.set(user.uri, user);
// Register host
- this.federatedInstanceService.fetch(host).then(async i => {
+ this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host);
}
});
@@ -520,8 +521,8 @@ export class ApPersonService implements OnModuleInit {
return undefined;
}
return 'private';
- })
- )
+ }),
+ ),
);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -595,6 +596,7 @@ export class ApPersonService implements OnModuleInit {
url,
fields,
description: _description,
+ followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
followingVisibility,
followersVisibility,
birthday: bday?.[0] ?? null,
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 2f58825de1..bb9836fb4e 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -13,6 +13,7 @@ export interface IObject {
name?: string | null;
summary?: string | null;
_misskey_summary?: string;
+ _misskey_followedMessage?: string | null;
published?: string;
cc?: ApObject;
to?: ApObject;
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index c2329a2f73..c9b43cc66d 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -5,10 +5,9 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
+import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
@@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
- private metaService: MetaService,
private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
@@ -43,8 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
}
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
- const meta = await this.metaService.fetch();
-
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
.where('instance.suspensionState != \'none\'');
@@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
- .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+ .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL')
- .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+ .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
- .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+ .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
.setParameters(pubsubSubQuery.getParameters())
@@ -88,7 +87,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
- .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+ .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
@@ -96,7 +95,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
- .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
+ .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 7695e6dfa7..721cff53bc 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -3,19 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js';
-import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js';
import { MiUser } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import { MiMeta } from '@/models/_.js';
@Injectable()
export class InstanceEntityService {
constructor(
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private roleService: RoleService,
private utilityService: UtilityService,
@@ -27,7 +30,6 @@ export class InstanceEntityService {
instance: MiInstance,
me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> {
- const meta = await this.metaService.fetch();
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
return {
@@ -41,7 +43,7 @@ export class InstanceEntityService {
isNotResponding: instance.isNotResponding,
isSuspended: instance.suspensionState !== 'none',
suspensionState: instance.suspensionState,
- isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
+ isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,
@@ -49,8 +51,8 @@ export class InstanceEntityService {
description: instance.description,
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
- isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
- isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
+ isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
+ isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index fa4ddc0bd6..61655c9652 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -9,7 +9,6 @@ import JSON5 from 'json5';
import type { Packed } from '@/misc/json-schema.js';
import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
@@ -23,11 +22,13 @@ export class MetaEntityService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
- private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { }
@@ -36,7 +37,7 @@ export class MetaEntityService {
let instance = meta;
if (!instance) {
- instance = await this.metaService.fetch();
+ instance = this.meta;
}
const ads = await this.adsRepository.createQueryBuilder('ads')
@@ -134,6 +135,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
+ maxFileSize: this.config.maxFileSize,
};
return packed;
@@ -144,7 +146,7 @@ export class MetaEntityService {
let instance = meta;
if (!instance) {
- instance = await this.metaService.fetch();
+ instance = this.meta;
}
const packed = await this.pack(instance);
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 493723ac45..4dd17c5af3 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -11,11 +11,11 @@ import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
-import type { MiNoteReaction } from '@/models/NoteReaction.js';
-import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
@@ -29,12 +29,16 @@ export class NoteEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService;
private reactionService: ReactionService;
+ private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor(
private moduleRef: ModuleRef,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -63,6 +67,8 @@ export class NoteEntityService implements OnModuleInit {
//private driveFileEntityService: DriveFileEntityService,
//private customEmojiService: CustomEmojiService,
//private reactionService: ReactionService,
+ //private reactionsBufferingService: ReactionsBufferingService,
+ //private idService: IdService,
) {
}
@@ -71,6 +77,7 @@ export class NoteEntityService implements OnModuleInit {
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService');
+ this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService');
}
@@ -119,7 +126,7 @@ export class NoteEntityService implements OnModuleInit {
followerId: meId,
},
});
-
+
hide = !isFollowing;
} else {
// フォロワーかどうか
@@ -304,6 +311,7 @@ export class NoteEntityService implements OnModuleInit {
skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: {
+ bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
@@ -320,6 +328,15 @@ export class NoteEntityService implements OnModuleInit {
const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
const host = note.userHost;
+ const bufferedReactions = opts._hint_?.bufferedReactions != null
+ ? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] })
+ : this.meta.enableReactionsBuffering
+ ? await this.reactionsBufferingService.get(note.id)
+ : { deltas: {}, pairs: [] };
+ const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
+
+ const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
+
let text = note.text;
if (note.name && (note.url ?? note.uri)) {
@@ -332,7 +349,7 @@ export class NoteEntityService implements OnModuleInit {
: await this.channelsRepository.findOneBy({ id: note.channelId })
: null;
- const reactionEmojiNames = Object.keys(note.reactions)
+ const reactionEmojiNames = Object.keys(reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
@@ -352,10 +369,10 @@ export class NoteEntityService implements OnModuleInit {
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
- reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
- reactions: this.reactionService.convertLegacyReactions(note.reactions),
+ reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
+ reactions: reactions,
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
- reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
+ reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds,
@@ -375,8 +392,12 @@ export class NoteEntityService implements OnModuleInit {
uri: note.uri ?? undefined,
url: note.url ?? undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
- ...(meId && Object.keys(note.reactions).length > 0 ? {
- myReaction: this.populateMyReaction(note, meId, options?._hint_),
+ ...(meId && Object.keys(reactions).length > 0 ? {
+ myReaction: this.populateMyReaction({
+ id: note.id,
+ reactions: reactions,
+ reactionAndUserPairCache: reactionAndUserPairCache,
+ }, meId, options?._hint_),
} : {}),
...(opts.detail ? {
@@ -416,6 +437,8 @@ export class NoteEntityService implements OnModuleInit {
) {
if (notes.length === 0) return [];
+ const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
+
const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) {
@@ -426,23 +449,33 @@ export class NoteEntityService implements OnModuleInit {
for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
- const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
+ const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
- } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
- const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
- myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
+ } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
+ const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
+ if (pairInBuffer) {
+ myReactionsMap.set(note.renote.id, pairInBuffer[1]);
+ } else {
+ const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
+ myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
+ }
} else {
idsNeedFetchMyReaction.add(note.renote.id);
}
} else {
if (note.id < oldId) {
- const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
+ const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
- } else if (reactionsCount <= note.reactionAndUserPairCache.length) {
- const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
- myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
+ } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
+ const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
+ if (pairInBuffer) {
+ myReactionsMap.set(note.id, pairInBuffer[1]);
+ } else {
+ const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
+ myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
+ }
} else {
idsNeedFetchMyReaction.add(note.id);
}
@@ -477,6 +510,7 @@ export class NoteEntityService implements OnModuleInit {
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
+ bufferedReactions,
myReactions: myReactionsMap,
packedFiles,
packedUsers,
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index e2de450756..8bfa2cc623 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],
- // eslint-disable-next-line @typescript-eslint/ban-types
+
options: {
checkValidNotifier?: boolean;
},
@@ -159,9 +159,16 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
+ ...(notification.type === 'followRequestAccepted' ? {
+ message: notification.message,
+ } : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
+ ...(notification.type === 'exportCompleted' ? {
+ exportedEntity: notification.exportedEntity,
+ fileId: notification.fileId,
+ } : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
@@ -229,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit {
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
- // eslint-disable-next-line @typescript-eslint/ban-types
+
options: {
checkValidNotifier?: boolean;
},
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index d465e2cd4c..bb05e8712f 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -556,7 +556,7 @@ export class UserEntityService implements OnModuleInit {
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
- }))
+ })),
) : undefined,
...(isDetailed ? {
@@ -613,6 +613,7 @@ export class UserEntityService implements OnModuleInit {
avatarId: user.avatarId,
bannerId: user.bannerId,
backgroundId: user.backgroundId,
+ followedMessage: profile!.followedMessage,
isModerator: isModerator,
isAdmin: isAdmin,
isSystem: isSystemAccount(user),
@@ -683,6 +684,7 @@ export class UserEntityService implements OnModuleInit {
isRenoteMuted: relation.isRenoteMuted,
notify: relation.following?.notify ?? 'none',
withReplies: relation.following?.withReplies ?? false,
+ followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
} : {}),
} as Promiseable<Packed<S>>;