/, '
');
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 1ddb2b173d..469426f87e 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -576,7 +576,14 @@ export class NoteCreateService implements OnApplicationShutdown {
noteId: 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 da76dd1284..a1e806816b 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';
@@ -38,6 +40,18 @@ import type {
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
+export const QUEUE_TYPES = [
+ 'system',
+ 'endedPollNotification',
+ 'deliver',
+ 'inbox',
+ 'db',
+ 'relationship',
+ 'objectStorage',
+ 'userWebhookDeliver',
+ 'systemWebhookDeliver',
+] as const;
+
@Injectable()
export class QueueService {
constructor(
@@ -57,50 +71,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,
});
}
@@ -122,13 +144,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,
+ },
});
}
@@ -150,12 +180,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,
@@ -176,13 +212,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,
+ },
});
}
@@ -191,8 +235,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,
+ },
});
}
@@ -201,8 +251,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,
+ },
});
}
@@ -211,8 +267,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,
+ },
});
}
@@ -221,8 +283,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,
+ },
});
}
@@ -231,8 +299,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,
+ },
});
}
@@ -243,8 +317,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,
+ },
});
}
@@ -253,8 +333,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,
+ },
});
}
@@ -263,8 +349,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,
+ },
});
}
@@ -273,8 +365,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,
+ },
});
}
@@ -283,8 +381,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,
+ },
});
}
@@ -295,8 +399,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,
+ },
});
}
@@ -312,8 +422,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,
+ },
});
}
@@ -323,8 +439,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,
+ },
});
}
@@ -344,8 +466,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,
+ },
},
};
}
@@ -356,8 +484,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,
+ },
});
}
@@ -367,8 +501,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,
+ },
});
}
@@ -378,8 +518,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,
+ },
});
}
@@ -389,8 +535,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,
+ },
});
}
@@ -440,8 +592,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,
},
};
@@ -452,16 +610,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,
+ },
});
}
@@ -492,8 +662,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,
+ },
});
}
@@ -523,21 +699,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}`);
+ }
+ }
- this.inboxQueue.once('cleaned', (jobs, status) => {
- //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
+ @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);
+ }
+ }
+
+ @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/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 601959cc96..fc97780ba3 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -46,6 +46,7 @@ export type RolePolicies = {
canUseTranslator: boolean;
canHideAds: boolean;
driveCapacityMb: number;
+ maxFileSizeMb: number;
alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number;
@@ -81,6 +82,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canUseTranslator: true,
canHideAds: false,
driveCapacityMb: 100,
+ maxFileSizeMb: 10,
alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5,
@@ -391,6 +393,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
+ maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
diff --git a/packages/backend/src/core/SystemAccountService.ts b/packages/backend/src/core/SystemAccountService.ts
index 1e050c3054..53c047dd74 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
RE: ...`
+ // 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);
+ });
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
@@ -509,7 +523,7 @@ export class ApRendererService {
const urlPart = match[0];
const urlPartParsed = new URL(urlPart);
const restPart = maybeUrl.slice(match[0].length);
-
+
return `${urlPart}${restPart}`;
} catch (e) {
return maybeUrl;
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index e252ff509e..d4769d24d4 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -486,8 +486,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),
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
id: ud.id,
angle: ud.angle || undefined,
@@ -533,8 +533,8 @@ export class UserEntityService implements OnModuleInit {
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
- bannerUrl: user.bannerUrl,
- bannerBlurhash: user.bannerBlurhash,
+ bannerUrl: user.bannerId == null ? null : user.bannerUrl,
+ bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended,
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index f8b828dda6..abaf615bcf 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())
@@ -65,7 +66,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 bc652cea62..baf4eefdf1 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -118,21 +118,25 @@ export class MiUser {
@JoinColumn()
public banner: 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,
})
public bannerUrl: 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 e852b302f3..e1ea2a2604 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,29 +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 { 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';
@@ -35,7 +54,6 @@ 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';
@@ -50,42 +68,38 @@ 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 { 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 type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository
+ {{ code }}
{{ code }}