summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/AccountMoveService.ts3
-rw-r--r--packages/backend/src/core/AntennaService.ts69
-rw-r--r--packages/backend/src/core/ChatService.ts4
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts2
-rw-r--r--packages/backend/src/core/MfmService.ts10
-rw-r--r--packages/backend/src/core/NoteCreateService.ts9
-rw-r--r--packages/backend/src/core/PushNotificationService.ts1
-rw-r--r--packages/backend/src/core/QueueService.ts498
-rw-r--r--packages/backend/src/core/SystemAccountService.ts46
-rw-r--r--packages/backend/src/core/WebhookTestService.ts12
-rw-r--r--packages/backend/src/core/activitypub/ApMfmService.ts10
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts20
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts12
-rw-r--r--packages/backend/src/models/Note.ts2
-rw-r--r--packages/backend/src/models/User.ts4
-rw-r--r--packages/backend/src/models/_.ts89
-rw-r--r--packages/backend/src/postgres.ts31
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts20
-rw-r--r--packages/backend/src/queue/const.ts10
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts48
-rw-r--r--packages/backend/src/server/ServerService.ts2
-rw-r--r--packages/backend/src/server/WellKnownServerService.ts2
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/clear.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/jobs.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/promote.ts77
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/queues.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/show-job.ts38
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts2
-rw-r--r--packages/backend/src/server/web/FeedService.ts2
34 files changed, 1012 insertions, 254 deletions
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 5128caff60..7bf33e13c5 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -25,6 +25,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { RoleService } from '@/core/RoleService.js';
+import { AntennaService } from '@/core/AntennaService.js';
@Injectable()
export class AccountMoveService {
@@ -66,6 +67,7 @@ export class AccountMoveService {
private queueService: QueueService,
private systemAccountService: SystemAccountService,
private roleService: RoleService,
+ private antennaService: AntennaService,
) {
}
@@ -127,6 +129,7 @@ export class AccountMoveService {
this.deleteScheduledNotes(src),
this.copyRoles(src, dst),
this.updateLists(src, dst),
+ this.antennaService.onMoveAccount(src, dst),
]);
} catch {
/* skip if any error happens */
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 13e3dcdbd8..cf696e3599 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -5,18 +5,20 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { MiAntenna } from '@/models/Antenna.js';
-import type { MiNote } from '@/models/Note.js';
-import type { MiUser } from '@/models/User.js';
+import { In } from 'typeorm';
+import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { bindThis } from '@/decorators.js';
+import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
-import { DI } from '@/di-symbols.js';
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
-import { UtilityService } from '@/core/UtilityService.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
+import type { MiAntenna } from '@/models/Antenna.js';
+import type { MiNote } from '@/models/Note.js';
+import type { MiUser } from '@/models/User.js';
+import { CacheService } from './CacheService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -37,6 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
+ private cacheService: CacheService,
private utilityService: UtilityService,
private globalEventService: GlobalEventService,
private fanoutTimelineService: FanoutTimelineService,
@@ -111,9 +114,6 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
- if (note.visibility === 'specified') return false;
- if (note.visibility === 'followers') return false;
-
if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false;
if (antenna.excludeBots && noteUser.isBot) return false;
@@ -122,6 +122,18 @@ export class AntennaService implements OnApplicationShutdown {
if (!antenna.withReplies && note.replyId != null) return false;
+ if (note.visibility === 'specified') {
+ if (note.userId !== antenna.userId) {
+ if (note.visibleUserIds == null) return false;
+ if (!note.visibleUserIds.includes(antenna.userId)) return false;
+ }
+ }
+
+ if (note.visibility === 'followers') {
+ const isFollowing = Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(antenna.userId), note.userId);
+ if (!isFollowing && antenna.userId !== note.userId) return false;
+ }
+
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
@@ -213,6 +225,41 @@ export class AntennaService implements OnApplicationShutdown {
}
@bindThis
+ public async onMoveAccount(src: MiUser, dst: MiUser): Promise<void> {
+ // There is a possibility for users to add the srcUser to their antennas, but it's low, so we don't check it.
+
+ // Get MiAntenna[] from cache and filter to select antennas with the src user is in the users list
+ const srcUserAcct = this.utilityService.getFullApAccount(src.username, src.host).toLowerCase();
+ const antennasToMigrate = (await this.getAntennas()).filter(antenna => {
+ return antenna.users.some(user => {
+ const { username, host } = Acct.parse(user);
+ return this.utilityService.getFullApAccount(username, host).toLowerCase() === srcUserAcct;
+ });
+ });
+
+ if (antennasToMigrate.length === 0) return;
+
+ const antennaIds = antennasToMigrate.map(x => x.id);
+
+ // Update the antennas by appending dst users acct to the users list
+ const dstUserAcct = '@' + Acct.toString({ username: dst.username, host: dst.host });
+
+ await this.antennasRepository.createQueryBuilder('antenna')
+ .update()
+ .set({
+ users: () => 'array_append(antenna.users, :dstUserAcct)',
+ })
+ .where('antenna.id IN (:...antennaIds)', { antennaIds })
+ .setParameters({ dstUserAcct })
+ .execute();
+
+ // announce update to event
+ for (const newAntenna of await this.antennasRepository.findBy({ id: In(antennaIds) })) {
+ this.globalEventService.publishInternalEvent('antennaUpdated', newAntenna);
+ }
+ }
+
+ @bindThis
public dispose(): void {
this.redisForSub.off('message', this.onRedisMessage);
}
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts
index b0e8cfb61c..9d294a80cb 100644
--- a/packages/backend/src/core/ChatService.ts
+++ b/packages/backend/src/core/ChatService.ts
@@ -232,7 +232,7 @@ export class ChatService {
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
- //this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
+ this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
}, 3000);
}
@@ -302,7 +302,7 @@ export class ChatService {
if (marker == null) continue;
this.globalEventService.publishMainStream(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
- //this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
+ this.pushNotificationService.pushNotification(membershipsOtherThanMe[i].userId, 'newChatMessage', packedMessageForTo);
}
}, 3000);
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index bd86a80cbd..84ca06ec1e 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -55,7 +55,7 @@ export class FanoutTimelineEndpointService {
}
@bindThis
- private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
+ async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 0ef52ee1a6..1ee3bd2275 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 } from 'happy-dom';
+import { type Document, type HTMLParagraphElement, Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
@@ -23,6 +23,8 @@ type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
+export type Appender = (document: Document, body: HTMLParagraphElement) => void;
+
@Injectable()
export class MfmService {
constructor(
@@ -343,7 +345,7 @@ export class MfmService {
}
@bindThis
- public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
+ public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
if (nodes == null) {
return null;
}
@@ -576,6 +578,10 @@ export class MfmService {
appendChildren(nodes, body);
+ for (const additionalAppender of additionalAppenders) {
+ additionalAppender(doc, body);
+ }
+
const serialized = body.outerHTML;
happyDOM.close().catch(err => {});
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 5543cc080d..fd6300483f 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -640,7 +640,14 @@ export class NoteCreateService implements OnApplicationShutdown {
}, {
jobId: `pollEnd:${note.id}`,
delay,
- removeOnComplete: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 1479bb00d9..9333c1ebc5 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -22,6 +22,7 @@ type PushNotificationsTypes = {
note: Packed<'Note'>;
};
'readAllNotifications': undefined;
+ newChatMessage: Packed<'ChatMessage'>;
};
// Reduce length because push message servers have character limits
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 039c47724b..fb0fa8f28d 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -5,6 +5,8 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
+import { MetricsTime, type JobType } from 'bullmq';
+import { parse as parseRedisInfo } from 'redis-info';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
@@ -40,6 +42,18 @@ import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import { MiNote } from '@/models/Note.js';
+export const QUEUE_TYPES = [
+ 'system',
+ 'endedPollNotification',
+ 'deliver',
+ 'inbox',
+ 'db',
+ 'relationship',
+ 'objectStorage',
+ 'userWebhookDeliver',
+ 'systemWebhookDeliver',
+] as const;
+
@Injectable()
export class QueueService {
constructor(
@@ -60,50 +74,58 @@ export class QueueService {
this.systemQueue.add('tickCharts', {
}, {
repeat: { pattern: '55 * * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('resyncCharts', {
}, {
repeat: { pattern: '0 0 * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('cleanCharts', {
}, {
repeat: { pattern: '0 0 * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('aggregateRetention', {
}, {
repeat: { pattern: '0 0 * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('clean', {
}, {
repeat: { pattern: '0 0 * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('checkExpiredMutings', {
}, {
repeat: { pattern: '*/5 * * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('bakeBufferedReactions', {
}, {
repeat: { pattern: '0 0 * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
this.systemQueue.add('checkModeratorsActivity', {
}, {
// 毎時30分に起動
repeat: { pattern: '30 * * * *' },
- removeOnComplete: true,
+ removeOnComplete: 10,
+ removeOnFail: 30,
});
}
@@ -125,13 +147,21 @@ export class QueueService {
isSharedInbox,
};
- return this.deliverQueue.add(to, data, {
+ const label = to.replace('https://', '').replace('/inbox', '');
+
+ return this.deliverQueue.add(label, data, {
attempts: this.config.deliverJobMaxAttempts ?? 12,
backoff: {
type: 'custom',
},
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -153,12 +183,18 @@ export class QueueService {
backoff: {
type: 'custom',
},
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
};
await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({
- name: d[0],
+ name: d[0].replace('https://', '').replace('/inbox', ''),
data: {
user,
content: contentBody,
@@ -179,13 +215,21 @@ export class QueueService {
signature,
};
- return this.inboxQueue.add('', data, {
+ const label = (activity.id ?? '').replace('https://', '').replace('/activity', '');
+
+ return this.inboxQueue.add(label, data, {
attempts: this.config.inboxJobMaxAttempts ?? 8,
backoff: {
type: 'custom',
},
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -194,8 +238,14 @@ export class QueueService {
return this.dbQueue.add('deleteDriveFiles', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -204,8 +254,14 @@ export class QueueService {
return this.dbQueue.add('exportCustomEmojis', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -224,8 +280,14 @@ export class QueueService {
return this.dbQueue.add('exportNotes', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -234,8 +296,14 @@ export class QueueService {
return this.dbQueue.add('exportClips', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -244,8 +312,14 @@ export class QueueService {
return this.dbQueue.add('exportFavorites', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -256,8 +330,14 @@ export class QueueService {
excludeMuting,
excludeInactive,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -266,8 +346,14 @@ export class QueueService {
return this.dbQueue.add('exportMuting', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -276,8 +362,14 @@ export class QueueService {
return this.dbQueue.add('exportBlocking', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -286,8 +378,14 @@ export class QueueService {
return this.dbQueue.add('exportUserLists', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -296,8 +394,14 @@ export class QueueService {
return this.dbQueue.add('exportAntennas', {
user: { id: user.id },
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -308,8 +412,14 @@ export class QueueService {
fileId: fileId,
withReplies,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -373,8 +483,14 @@ export class QueueService {
user: { id: user.id },
fileId: fileId,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -384,8 +500,14 @@ export class QueueService {
user: { id: user.id },
fileId: fileId,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -405,8 +527,14 @@ export class QueueService {
name,
data,
opts: {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
},
};
}
@@ -417,8 +545,14 @@ export class QueueService {
user: { id: user.id },
fileId: fileId,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -428,8 +562,14 @@ export class QueueService {
user: { id: user.id },
fileId: fileId,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -439,8 +579,14 @@ export class QueueService {
user: { id: user.id },
antenna,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -450,8 +596,14 @@ export class QueueService {
user: { id: user.id },
soft: opts.soft,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -501,8 +653,14 @@ export class QueueService {
withReplies: data.withReplies,
},
opts: {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
...opts,
},
};
@@ -513,16 +671,28 @@ export class QueueService {
return this.objectStorageQueue.add('deleteFile', {
key: key,
}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@bindThis
public createCleanRemoteFilesJob() {
return this.objectStorageQueue.add('cleanRemoteFiles', {}, {
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -553,8 +723,14 @@ export class QueueService {
backoff: {
type: 'custom',
},
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@@ -584,21 +760,201 @@ export class QueueService {
backoff: {
type: 'custom',
},
- removeOnComplete: true,
- removeOnFail: true,
+ removeOnComplete: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 30,
+ },
+ removeOnFail: {
+ age: 3600 * 24 * 7, // keep up to 7 days
+ count: 100,
+ },
});
}
@bindThis
- public destroy() {
- this.deliverQueue.once('cleaned', (jobs, status) => {
- //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
- });
- this.deliverQueue.clean(0, 0, 'delayed');
+ private getQueue(type: typeof QUEUE_TYPES[number]): Bull.Queue {
+ switch (type) {
+ case 'system': return this.systemQueue;
+ case 'endedPollNotification': return this.endedPollNotificationQueue;
+ case 'deliver': return this.deliverQueue;
+ case 'inbox': return this.inboxQueue;
+ case 'db': return this.dbQueue;
+ case 'relationship': return this.relationshipQueue;
+ case 'objectStorage': return this.objectStorageQueue;
+ case 'userWebhookDeliver': return this.userWebhookDeliverQueue;
+ case 'systemWebhookDeliver': return this.systemWebhookDeliverQueue;
+ default: throw new Error(`Unrecognized queue type: ${type}`);
+ }
+ }
+
+ @bindThis
+ public async queueClear(queueType: typeof QUEUE_TYPES[number], state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed') {
+ const queue = this.getQueue(queueType);
+
+ if (state === '*') {
+ await Promise.all([
+ queue.clean(0, 0, 'completed'),
+ queue.clean(0, 0, 'wait'),
+ queue.clean(0, 0, 'active'),
+ queue.clean(0, 0, 'paused'),
+ queue.clean(0, 0, 'prioritized'),
+ queue.clean(0, 0, 'delayed'),
+ queue.clean(0, 0, 'failed'),
+ ]);
+ } else {
+ await queue.clean(0, 0, state);
+ }
+ }
- this.inboxQueue.once('cleaned', (jobs, status) => {
- //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
+ @bindThis
+ public async queuePromoteJobs(queueType: typeof QUEUE_TYPES[number]) {
+ const queue = this.getQueue(queueType);
+ await queue.promoteJobs();
+ }
+
+ @bindThis
+ public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
+ const queue = this.getQueue(queueType);
+ const job: Bull.Job | null = await queue.getJob(jobId);
+ if (job) {
+ if (job.finishedOn != null) {
+ await job.retry();
+ } else {
+ await job.promote();
+ }
+ }
+ }
+
+ @bindThis
+ public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
+ const queue = this.getQueue(queueType);
+ const job: Bull.Job | null = await queue.getJob(jobId);
+ if (job) {
+ await job.remove();
+ }
+ }
+
+ @bindThis
+ private packJobData(job: Bull.Job) {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : [];
+ stacktrace.reverse();
+
+ return {
+ id: job.id,
+ name: job.name,
+ data: job.data,
+ opts: job.opts,
+ timestamp: job.timestamp,
+ processedOn: job.processedOn,
+ processedBy: job.processedBy,
+ finishedOn: job.finishedOn,
+ progress: job.progress,
+ attempts: job.attemptsMade,
+ delay: job.delay,
+ failedReason: job.failedReason,
+ stacktrace: stacktrace,
+ returnValue: job.returnvalue,
+ isFailed: !!job.failedReason || (Array.isArray(stacktrace) && stacktrace.length > 0),
+ };
+ }
+
+ @bindThis
+ public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
+ const queue = this.getQueue(queueType);
+ const job: Bull.Job | null = await queue.getJob(jobId);
+ if (job) {
+ return this.packJobData(job);
+ } else {
+ throw new Error(`Job not found: ${jobId}`);
+ }
+ }
+
+ @bindThis
+ public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) {
+ const RETURN_LIMIT = 100;
+ const queue = this.getQueue(queueType);
+ let jobs: Bull.Job[];
+
+ if (search) {
+ jobs = await queue.getJobs(jobTypes, 0, 1000);
+
+ jobs = jobs.filter(job => {
+ const jobString = JSON.stringify(job).toLowerCase();
+ return search.toLowerCase().split(' ').every(term => {
+ return jobString.includes(term);
+ });
+ });
+
+ jobs = jobs.slice(0, RETURN_LIMIT);
+ } else {
+ jobs = await queue.getJobs(jobTypes, 0, RETURN_LIMIT);
+ }
+
+ return jobs.map(job => this.packJobData(job));
+ }
+
+ @bindThis
+ public async queueGetQueues() {
+ const fetchings = QUEUE_TYPES.map(async type => {
+ const queue = this.getQueue(type);
+
+ const counts = await queue.getJobCounts();
+ const isPaused = await queue.isPaused();
+ const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK);
+ const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK);
+
+ return {
+ name: type,
+ counts: counts,
+ isPaused,
+ metrics: {
+ completed: metrics_completed,
+ failed: metrics_failed,
+ },
+ };
});
- this.inboxQueue.clean(0, 0, 'delayed');
+
+ return await Promise.all(fetchings);
+ }
+
+ @bindThis
+ public async queueGetQueue(queueType: typeof QUEUE_TYPES[number]) {
+ const queue = this.getQueue(queueType);
+ const counts = await queue.getJobCounts();
+ const isPaused = await queue.isPaused();
+ const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK);
+ const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK);
+ const db = parseRedisInfo(await (await queue.client).info());
+
+ return {
+ name: queueType,
+ qualifiedName: queue.qualifiedName,
+ counts: counts,
+ isPaused,
+ metrics: {
+ completed: metrics_completed,
+ failed: metrics_failed,
+ },
+ db: {
+ version: db.redis_version,
+ mode: db.redis_mode,
+ runId: db.run_id,
+ processId: db.process_id,
+ port: parseInt(db.tcp_port),
+ os: db.os,
+ uptime: parseInt(db.uptime_in_seconds),
+ memory: {
+ total: parseInt(db.total_system_memory) || parseInt(db.maxmemory),
+ used: parseInt(db.used_memory),
+ fragmentationRatio: parseInt(db.mem_fragmentation_ratio),
+ peak: parseInt(db.used_memory_peak),
+ },
+ clients: {
+ connected: parseInt(db.connected_clients),
+ blocked: parseInt(db.blocked_clients),
+ },
+ },
+ };
}
}
diff --git a/packages/backend/src/core/SystemAccountService.ts b/packages/backend/src/core/SystemAccountService.ts
index 3f3afe6561..1288dc6ffa 100644
--- a/packages/backend/src/core/SystemAccountService.ts
+++ b/packages/backend/src/core/SystemAccountService.ts
@@ -5,11 +5,14 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
+import type { OnApplicationShutdown } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
+import * as Redis from 'ioredis';
import bcrypt from 'bcryptjs';
import { MiLocalUser, MiUser } from '@/models/User.js';
import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js';
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +23,13 @@ import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const;
@Injectable()
-export class SystemAccountService {
+export class SystemAccountService implements OnApplicationShutdown {
private cache: MemoryKVCache<MiLocalUser>;
constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+
@Inject(DI.db)
private db: DataSource,
@@ -42,6 +48,31 @@ export class SystemAccountService {
private idService: IdService,
) {
this.cache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 10); // 10m
+
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+
+ if (obj.channel === 'internal') {
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'metaUpdated': {
+ if (body.before != null && body.before.name !== body.after.name) {
+ for (const account of SYSTEM_ACCOUNT_TYPES) {
+ await this.updateCorrespondingUserProfile(account, {
+ name: body.after.name,
+ });
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
}
@bindThis
@@ -151,7 +182,7 @@ export class SystemAccountService {
@bindThis
public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
- name?: string;
+ name?: string | null;
description?: MiUserProfile['description'];
}): Promise<MiLocalUser> {
const user = await this.fetch(type);
@@ -187,4 +218,15 @@ export class SystemAccountService {
public async getProxyActor() {
return await this.fetch('proxy');
}
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ this.cache.dispose();
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 469b396fb0..2f8cfea7f7 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -431,8 +431,8 @@ export class WebhookTestService {
name: user.name,
username: user.username,
host: user.host,
- avatarUrl: user.avatarUrl,
- avatarBlurhash: user.avatarBlurhash,
+ avatarUrl: user.avatarId == null ? null : user.avatarUrl,
+ avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
angle: it.angle,
@@ -464,10 +464,10 @@ export class WebhookTestService {
createdAt: new Date().toISOString(),
updatedAt: user.updatedAt?.toISOString() ?? null,
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
- bannerUrl: user.bannerUrl,
- bannerBlurhash: user.bannerBlurhash,
- backgroundUrl: user.backgroundUrl,
- backgroundBlurhash: user.backgroundBlurhash,
+ bannerUrl: user.bannerId == null ? null : user.bannerUrl,
+ bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
+ backgroundUrl: user.backgroundId == null ? null : user.backgroundUrl,
+ backgroundBlurhash: user.backgroundId == null ? null : user.backgroundBlurhash,
listenbrainz: null,
isLocked: user.isLocked,
isSilenced: false,
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index 318710fa93..c4a948429a 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import * as mfm from '@transfem-org/sfm-js';
-import { MfmService } from '@/core/MfmService.js';
+import { MfmService, Appender } from '@/core/MfmService.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js';
@@ -25,17 +25,17 @@ export class ApMfmService {
}
@bindThis
- public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
+ public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, additionalAppender: Appender[] = []) {
let noMisskeyContent = false;
- const srcMfm = (note.text ?? '') + (apAppend ?? '');
+ const srcMfm = (note.text ?? '');
const parsed = mfm.parse(srcMfm);
- if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
+ if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true;
}
- const content = this.mfmService.toHtml(parsed, note.mentionedRemoteUsers ? JSON.parse(note.mentionedRemoteUsers) : []);
+ const content = this.mfmService.toHtml(parsed, note.mentionedRemoteUsers ? JSON.parse(note.mentionedRemoteUsers) : [], additionalAppender);
return {
content,
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index e6eb777f1b..8251bc3b15 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -20,7 +20,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiPollVote } from '@/models/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { MfmService } from '@/core/MfmService.js';
+import { MfmService, type Appender } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js';
@@ -468,10 +468,24 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
- let apAppend = '';
+ const apAppend: Appender[] = [];
if (quote) {
- apAppend += `\n\nRE: ${quote}`;
+ // Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>`
+ // the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
+ // For compatibility, the span part should be kept as possible.
+ apAppend.push((doc, body) => {
+ body.appendChild(doc.createElement('br'));
+ body.appendChild(doc.createElement('br'));
+ const span = doc.createElement('span');
+ span.className = 'quote-inline';
+ span.appendChild(doc.createTextNode('RE: '));
+ const link = doc.createElement('a');
+ link.setAttribute('href', quote);
+ link.textContent = quote;
+ span.appendChild(link);
+ body.appendChild(span);
+ });
}
let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index d11042d73b..16425bb6ce 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -584,8 +584,8 @@ export class UserEntityService implements OnModuleInit {
name: user.name,
username: user.username,
host: user.host,
- avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
- avatarBlurhash: user.avatarBlurhash,
+ avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? this.getIdenticonUrl(user),
+ avatarBlurhash: (user.avatarId == null ? null : user.avatarBlurhash),
description: mastoapi ? mastoapi.description : profile ? profile.description : '',
createdAt: this.idService.parse(user.id).date.toISOString(),
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
@@ -643,10 +643,10 @@ export class UserEntityService implements OnModuleInit {
: null,
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
- bannerUrl: user.bannerUrl,
- bannerBlurhash: user.bannerBlurhash,
- backgroundUrl: user.backgroundUrl,
- backgroundBlurhash: user.backgroundBlurhash,
+ bannerUrl: user.bannerId == null ? null : user.bannerUrl,
+ bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
+ backgroundUrl: user.backgroundUrl == null ? null : user.backgroundUrl,
+ backgroundBlurhash: user.backgroundBlurhash == null ? null : user.backgroundBlurhash,
isLocked: user.isLocked,
isSuspended: user.isSuspended,
location: profile!.location,
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index 2dabb75d83..9328e9ebae 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -10,6 +10,7 @@ import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import type { MiDriveFile } from './DriveFile.js';
+@Index(['userId', 'id'])
@Entity('note')
export class MiNote {
@PrimaryColumn(id())
@@ -71,7 +72,6 @@ export class MiNote {
})
public cw: string | null;
- @Index()
@Column({
...id(),
comment: 'The ID of author.',
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 0678746e26..760ef52d2b 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -132,11 +132,13 @@ export class MiUser {
@JoinColumn()
public background: MiDriveFile | null;
+ // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
@Column('varchar', {
length: 512, nullable: true,
})
public avatarUrl: string | null;
+ // bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること
@Column('varchar', {
length: 512, nullable: true,
})
@@ -147,11 +149,13 @@ export class MiUser {
})
public backgroundUrl: string | null;
+ // avatarId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは avatarId の non-null チェックをすること
@Column('varchar', {
length: 128, nullable: true,
})
public avatarBlurhash: string | null;
+ // bannerId が null になったとしてもこれが null でない可能性があるため、このフィールドを使うときは bannerId の non-null チェックをすること
@Column('varchar', {
length: 128, nullable: true,
})
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 0ec619f38d..225e8ac025 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,30 +3,48 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
+import {
+ FindOneOptions,
+ InsertQueryBuilder,
+ ObjectLiteral,
+ QueryRunner,
+ Repository,
+ SelectQueryBuilder,
+} from 'typeorm';
+import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
-import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
-import { SkLatestNote } from '@/models/LatestNote.js';
-import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import {
+ RawSqlResultsToEntityTransformer,
+} from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
+import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js';
import { MiApp } from '@/models/App.js';
-import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiAuthSession } from '@/models/AuthSession.js';
+import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiBlocking } from '@/models/Blocking.js';
-import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
+import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
+import { MiChannel } from '@/models/Channel.js';
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
+import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
+import { MiChatApproval } from '@/models/ChatApproval.js';
+import { MiChatMessage } from '@/models/ChatMessage.js';
+import { MiChatRoom } from '@/models/ChatRoom.js';
+import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
+import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
import { MiClip } from '@/models/Clip.js';
-import { MiClipNote } from '@/models/ClipNote.js';
import { MiClipFavorite } from '@/models/ClipFavorite.js';
+import { MiClipNote } from '@/models/ClipNote.js';
import { MiDriveFile } from '@/models/DriveFile.js';
import { MiDriveFolder } from '@/models/DriveFolder.js';
import { MiEmoji } from '@/models/Emoji.js';
+import { MiFlash } from '@/models/Flash.js';
+import { MiFlashLike } from '@/models/FlashLike.js';
import { MiFollowing } from '@/models/Following.js';
import { MiFollowRequest } from '@/models/FollowRequest.js';
import { MiGalleryLike } from '@/models/GalleryLike.js';
@@ -36,10 +54,10 @@ import { MiInstance } from '@/models/Instance.js';
import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMuting } from '@/models/Muting.js';
-import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
+import { MiNoteSchedule } from '@/models/NoteSchedule.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js';
@@ -51,47 +69,43 @@ import { MiPromoRead } from '@/models/PromoRead.js';
import { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { MiRegistryItem } from '@/models/RegistryItem.js';
import { MiRelay } from '@/models/Relay.js';
+import { MiRenoteMuting } from '@/models/RenoteMuting.js';
+import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
+import { MiReversiGame } from '@/models/ReversiGame.js';
+import { MiRole } from '@/models/Role.js';
+import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiSignin } from '@/models/Signin.js';
import { MiSwSubscription } from '@/models/SwSubscription.js';
import { MiSystemAccount } from '@/models/SystemAccount.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import { MiUser } from '@/models/User.js';
import { MiUserIp } from '@/models/UserIp.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUserList } from '@/models/UserList.js';
+import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiUserListMembership } from '@/models/UserListMembership.js';
+import { MiUserMemo } from '@/models/UserMemo.js';
import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
-import { MiUserMemo } from '@/models/UserMemo.js';
import { MiWebhook } from '@/models/Webhook.js';
-import { MiSystemWebhook } from '@/models/SystemWebhook.js';
-import { MiChannel } from '@/models/Channel.js';
-import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
-import { MiRole } from '@/models/Role.js';
-import { MiRoleAssignment } from '@/models/RoleAssignment.js';
-import { MiFlash } from '@/models/Flash.js';
-import { MiFlashLike } from '@/models/FlashLike.js';
-import { MiUserListFavorite } from '@/models/UserListFavorite.js';
+import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
import { NoteEdit } from '@/models/NoteEdit.js';
-import { MiChatMessage } from '@/models/ChatMessage.js';
-import { MiChatRoom } from '@/models/ChatRoom.js';
-import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
-import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
-import { MiChatApproval } from '@/models/ChatApproval.js';
-import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
-import { MiReversiGame } from '@/models/ReversiGame.js';
-import { MiNoteSchedule } from '@/models/NoteSchedule.js';
import { SkApInboxLog } from '@/models/SkApInboxLog.js';
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
import { SkApContext } from '@/models/SkApContext.js';
-import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
+import { SkLatestNote } from '@/models/LatestNote.js';
export interface MiRepository<T extends ObjectLiteral> {
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
+
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
+
+ insertOneImpl(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>, queryRunner?: QueryRunner): Promise<T>;
+
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
}
@@ -100,6 +114,21 @@ export const miRepository = {
return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
},
async insertOne(entity, findOptions?) {
+ const opt = this.manager.connection.options as PostgresConnectionOptions;
+ if (opt.replication) {
+ const queryRunner = this.manager.connection.createQueryRunner('master');
+ try {
+ return this.insertOneImpl(entity, findOptions, queryRunner);
+ } finally {
+ await queryRunner.release();
+ }
+ } else {
+ return this.insertOneImpl(entity, findOptions);
+ }
+ },
+ async insertOneImpl(entity, findOptions?, queryRunner?) {
+ // ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ----
+
const queryBuilder = this.createQueryBuilder().insert().values(entity);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mainAlias = queryBuilder.expressionMap.mainAlias!;
@@ -107,7 +136,9 @@ export const miRepository = {
mainAlias.name = 't';
const columnNames = this.createTableColumnNames();
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
- const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
+
+ // ---- 共通テーブル式(CTE)から結果を取得 ----
+ const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
builder.expressionMap.mainAlias!.tablePath = 'cte';
this.selectAliasColumnNames(queryBuilder, builder);
@@ -216,7 +247,9 @@ export {
};
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
-export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
+export type AbuseReportNotificationRecipientRepository =
+ Repository<MiAbuseReportNotificationRecipient>
+ & MiRepository<MiAbuseReportNotificationRecipient>;
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index ffc2daa62c..632fd58927 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -5,7 +5,7 @@
// https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg';
-import { DataSource, Logger } from 'typeorm';
+import { DataSource, Logger, type QueryRunner } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { Config } from '@/config.js';
@@ -102,6 +102,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
export type LoggerProps = {
disableQueryTruncation?: boolean;
enableQueryParamLogging?: boolean;
+ printReplicationMode?: boolean,
};
function highlightSql(sql: string) {
@@ -127,8 +128,10 @@ class MyCustomLogger implements Logger {
}
@bindThis
- private transformQueryLog(sql: string) {
- let modded = sql;
+ private transformQueryLog(sql: string, opts?: {
+ prefix?: string;
+ }) {
+ let modded = opts?.prefix ? opts.prefix + sql : sql;
if (!this.props.disableQueryTruncation) {
modded = truncateSql(modded);
}
@@ -146,18 +149,27 @@ class MyCustomLogger implements Logger {
}
@bindThis
- public logQuery(query: string, parameters?: any[]) {
- sqlLogger.info(this.transformQueryLog(query), this.transformParameters(parameters));
+ public logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
+ const prefix = (this.props.printReplicationMode && queryRunner)
+ ? `[${queryRunner.getReplicationMode()}] `
+ : undefined;
+ sqlLogger.info(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
}
@bindThis
- public logQueryError(error: string, query: string, parameters?: any[]) {
- sqlLogger.error(this.transformQueryLog(query), this.transformParameters(parameters));
+ public logQueryError(error: string, query: string, parameters?: any[], queryRunner?: QueryRunner) {
+ const prefix = (this.props.printReplicationMode && queryRunner)
+ ? `[${queryRunner.getReplicationMode()}] `
+ : undefined;
+ sqlLogger.error(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
}
@bindThis
- public logQuerySlow(time: number, query: string, parameters?: any[]) {
- sqlLogger.warn(this.transformQueryLog(query), this.transformParameters(parameters));
+ public logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) {
+ const prefix = (this.props.printReplicationMode && queryRunner)
+ ? `[${queryRunner.getReplicationMode()}] `
+ : undefined;
+ sqlLogger.warn(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
}
@bindThis
@@ -306,6 +318,7 @@ export function createPostgresDataSource(config: Config) {
? new MyCustomLogger({
disableQueryTruncation: config.logging?.sql?.disableQueryTruncation,
enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging,
+ printReplicationMode: !!config.dbReplications,
})
: undefined,
maxQueryExecutionTime: 300,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 297edfd545..90aaf67a84 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -47,7 +47,7 @@ import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
-import { QUEUE, baseQueueOptions } from './const.js';
+import { QUEUE, baseWorkerOptions } from './const.js';
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
@@ -186,7 +186,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return processer(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.SYSTEM),
+ ...baseWorkerOptions(this.config, QUEUE.SYSTEM),
autorun: false,
});
@@ -251,7 +251,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return processer(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.DB),
+ ...baseWorkerOptions(this.config, QUEUE.DB),
autorun: false,
});
@@ -283,7 +283,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return this.deliverProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.DELIVER),
+ ...baseWorkerOptions(this.config, QUEUE.DELIVER),
autorun: false,
concurrency: this.config.deliverJobConcurrency ?? 128,
limiter: {
@@ -323,7 +323,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return this.inboxProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.INBOX),
+ ...baseWorkerOptions(this.config, QUEUE.INBOX),
autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: {
@@ -363,7 +363,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return this.userWebhookDeliverProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
+ ...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
autorun: false,
concurrency: 64,
limiter: {
@@ -403,7 +403,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return this.systemWebhookDeliverProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
+ ...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
autorun: false,
concurrency: 16,
limiter: {
@@ -453,7 +453,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return processer(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
+ ...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
@@ -498,7 +498,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return processer(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
+ ...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE),
autorun: false,
concurrency: 16,
});
@@ -531,7 +531,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
return this.endedPollNotificationProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
+ ...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
autorun: false,
});
}
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
index fdf012f149..17c6b81736 100644
--- a/packages/backend/src/queue/const.ts
+++ b/packages/backend/src/queue/const.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { MetricsTime } from 'bullmq';
import { Config } from '@/config.js';
import type * as Bull from 'bullmq';
@@ -28,3 +29,12 @@ export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof t
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
};
}
+
+export function baseWorkerOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.WorkerOptions {
+ return {
+ ...baseQueueOptions(config, queueName),
+ metrics: {
+ maxDataPoints: MetricsTime.ONE_WEEK,
+ },
+ };
+}
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index c7aa694964..95ee78d660 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -38,6 +38,7 @@ import * as Acct from '@/misc/acct.js';
import { CacheService } from '@/core/CacheService.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
+import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@@ -85,6 +86,7 @@ export class ActivityPubServerService {
private queueService: QueueService,
private userKeypairService: UserKeypairService,
private queryService: QueryService,
+ private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private loggerService: LoggerService,
private readonly cacheService: CacheService,
) {
@@ -613,16 +615,28 @@ export class ActivityPubServerService {
const partOf = `${this.config.url}/users/${userId}/outbox`;
if (page) {
- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
- .andWhere('note.userId = :userId', { userId: user.id })
- .andWhere(new Brackets(qb => {
- qb
- .where('note.visibility = \'public\'')
- .orWhere('note.visibility = \'home\'');
- }))
- .andWhere('note.localOnly = FALSE');
-
- const notes = await query.limit(limit).getMany();
+ const notes = this.meta.enableFanoutTimeline ? await this.fanoutTimelineEndpointService.getMiNotes({
+ sinceId: sinceId ?? null,
+ untilId: untilId ?? null,
+ limit: limit,
+ allowPartial: false, // Possibly true? IDK it's OK for ordered collection.
+ me: null,
+ redisTimelines: [
+ `userTimeline:${user.id}`,
+ `userTimelineWithReplies:${user.id}`,
+ ],
+ useDbFallback: true,
+ ignoreAuthorFromMute: true,
+ excludePureRenotes: false,
+ noteFilter: (note) => {
+ if (note.visibility !== 'home' && note.visibility !== 'public') return false;
+ if (note.localOnly) return false;
+ return true;
+ },
+ dbFallback: async (untilId, sinceId, limit) => {
+ return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
+ },
+ }) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
if (sinceId) notes.reverse();
@@ -660,6 +674,20 @@ export class ActivityPubServerService {
}
@bindThis
+ private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
+ return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
+ .andWhere('note.userId = :userId', { userId })
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('note.visibility = \'public\'')
+ .orWhere('note.visibility = \'home\'');
+ }))
+ .andWhere('note.localOnly = FALSE')
+ .limit(limit)
+ .getMany();
+ }
+
+ @bindThis
private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null, redact = false) {
if (this.meta.federation === 'none') {
reply.code(403);
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index c90c206e94..dce47e2290 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -223,7 +223,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400');
if (user) {
- reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
+ reply.redirect((user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user));
} else {
reply.redirect('/static-assets/user-unknown.png');
}
diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts
index 25fa725320..f48310c50f 100644
--- a/packages/backend/src/server/WellKnownServerService.ts
+++ b/packages/backend/src/server/WellKnownServerService.ts
@@ -138,7 +138,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<MiUser> | number =>
!acct.host || acct.host === this.config.host.toLowerCase() ? {
- usernameLower: acct.username,
+ usernameLower: acct.username.toLowerCase(),
host: IsNull(),
isSuspended: false,
} : 422;
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index e48b3de8ee..1c5a781fd9 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -72,8 +72,14 @@ export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
-export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
+export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js';
+export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js';
+export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
+export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
+export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
+export * as 'admin/queue/queues' from './endpoints/admin/queue/queues.js';
+export * as 'admin/queue/queue-stats' from './endpoints/admin/queue/queue-stats.js';
export * as 'admin/reject-quotes' from './endpoints/admin/reject-quotes.js';
export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
index 3f7df0e63d..81cb4b8119 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
@@ -6,7 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { QueueService } from '@/core/QueueService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
@@ -18,8 +18,11 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
- required: [],
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ state: { type: 'string', enum: ['*', 'completed', 'wait', 'active', 'paused', 'prioritized', 'delayed', 'failed'] },
+ },
+ required: ['queue', 'state'],
} as const;
@Injectable()
@@ -29,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.destroy();
+ this.queueService.queueClear(ps.queue, ps.state);
this.moderationLogService.log(me, 'clearQueue');
});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
new file mode 100644
index 0000000000..79731c9786
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'read:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
+ search: { type: 'string' },
+ },
+ required: ['queue', 'state'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ return this.queueService.queueGetJobs(ps.queue, ps.state, ps.search);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts
new file mode 100644
index 0000000000..d22385e261
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/promote-jobs.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ },
+ required: ['queue'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ this.queueService.queuePromoteJobs(ps.queue);
+
+ this.moderationLogService.log(me, 'promoteQueue');
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts
deleted file mode 100644
index 7502d4e1f7..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { QueueService } from '@/core/QueueService.js';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true,
- requireModerator: true,
- kind: 'write:admin:queue',
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- type: { type: 'string', enum: ['deliver', 'inbox'] },
- },
- required: ['type'],
-} as const;
-
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
- constructor(
- private moderationLogService: ModerationLogService,
- private queueService: QueueService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- let delayedQueues;
-
- switch (ps.type) {
- case 'deliver':
- delayedQueues = await this.queueService.deliverQueue.getDelayed();
- for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
- const queue = delayedQueues[queueIndex];
- try {
- await queue.promote();
- } catch (e) {
- if (e instanceof Error) {
- if (e.message.indexOf('not in a delayed state') !== -1) {
- throw e;
- }
- } else {
- throw e;
- }
- }
- }
- break;
-
- case 'inbox':
- delayedQueues = await this.queueService.inboxQueue.getDelayed();
- for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
- const queue = delayedQueues[queueIndex];
- try {
- await queue.promote();
- } catch (e) {
- if (e instanceof Error) {
- if (e.message.indexOf('not in a delayed state') !== -1) {
- throw e;
- }
- } else {
- throw e;
- }
- }
- }
- break;
- }
-
- this.moderationLogService.log(me, 'promoteQueue');
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts
new file mode 100644
index 0000000000..10ce48332a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'read:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ },
+ required: ['queue'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ return this.queueService.queueGetQueue(ps.queue);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts
new file mode 100644
index 0000000000..3a38275f60
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts
@@ -0,0 +1,35 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'read:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ return this.queueService.queueGetQueues();
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts
new file mode 100644
index 0000000000..2c73f689d0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/remove-job.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ jobId: { type: 'string' },
+ },
+ required: ['queue', 'jobId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ this.queueService.queueRemoveJob(ps.queue, ps.jobId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts
new file mode 100644
index 0000000000..b2603128f8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/retry-job.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ jobId: { type: 'string' },
+ },
+ required: ['queue', 'jobId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ this.queueService.queueRetryJob(ps.queue, ps.jobId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts
new file mode 100644
index 0000000000..63747b5540
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'read:admin:queue',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ queue: { type: 'string', enum: QUEUE_TYPES },
+ jobId: { type: 'string' },
+ },
+ required: ['queue', 'jobId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ return this.queueService.queueGetJob(ps.queue, ps.jobId);
+ });
+ }
+}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 34b69fd7b2..1321cf6338 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -558,7 +558,7 @@ export class ClientServerService {
return await reply.view('user', {
user, profile, me,
- avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
+ avatarUrl: _user.avatarUrl,
sub: request.params.sub,
...await this.generateCommonPugData(this.meta),
clientCtx: htmlSafeJsonStringify({
diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts
index 664f21d308..dcd4d80303 100644
--- a/packages/backend/src/server/web/FeedService.ts
+++ b/packages/backend/src/server/web/FeedService.ts
@@ -65,7 +65,7 @@ export class FeedService {
generator: 'Sharkey',
description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link,
- image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
+ image: (user.avatarId == null ? null : user.avatarUrl) ?? this.userEntityService.getIdenticonUrl(user),
feedLinks: {
json: `${author.link}.json`,
atom: `${author.link}.atom`,