Merge tag '13.13.2' into merge-upstream
This commit is contained in:
@@ -4,7 +4,7 @@ import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { MeiliSearch } from 'meilisearch';
|
||||
import { DI } from './di-symbols.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { Config, loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
@@ -25,7 +25,7 @@ const $db: Provider = {
|
||||
|
||||
const $meilisearch: Provider = {
|
||||
provide: DI.meilisearch,
|
||||
useFactory: (config) => {
|
||||
useFactory: (config: Config) => {
|
||||
if (config.meilisearch) {
|
||||
return new MeiliSearch({
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
@@ -40,7 +40,7 @@ const $meilisearch: Provider = {
|
||||
|
||||
const $redis: Provider = {
|
||||
provide: DI.redis,
|
||||
useFactory: (config) => {
|
||||
useFactory: (config: Config) => {
|
||||
return new Redis.Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
@@ -55,7 +55,7 @@ const $redis: Provider = {
|
||||
|
||||
const $redisForPub: Provider = {
|
||||
provide: DI.redisForPub,
|
||||
useFactory: (config) => {
|
||||
useFactory: (config: Config) => {
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
@@ -71,7 +71,7 @@ const $redisForPub: Provider = {
|
||||
|
||||
const $redisForSub: Provider = {
|
||||
provide: DI.redisForSub,
|
||||
useFactory: (config) => {
|
||||
useFactory: (config: Config) => {
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
@@ -100,7 +100,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
) {}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
@@ -116,4 +116,8 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||
this.redisForSub.disconnect(),
|
||||
]);
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -146,7 +146,7 @@ export function loadConfig() {
|
||||
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
|
||||
const clientManifest = clientManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/init.ts': { file: 'src/init.ts' } };
|
||||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
|
||||
const mixin = {} as Mixin;
|
||||
@@ -167,7 +167,7 @@ export function loadConfig() {
|
||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
|
||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
||||
mixin.clientEntry = clientManifest['src/_boot_.ts'];
|
||||
mixin.clientManifestExists = clientManifestExists;
|
||||
|
||||
const externalMediaProxy = config.mediaProxy ?
|
||||
@@ -192,6 +192,6 @@ function tryCreateUrl(url: string) {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
throw `url="${url}" is not a valid URL.`;
|
||||
throw new Error(`url="${url}" is not a valid URL.`);
|
||||
}
|
||||
}
|
||||
|
@@ -55,11 +55,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
this.redisForSub.on('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForSub.off('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onRedisMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
@@ -196,4 +191,14 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
return this.antennas;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -166,7 +166,23 @@ export class CacheService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
this.userByIdCache.dispose();
|
||||
this.localUserByNativeTokenCache.dispose();
|
||||
this.localUserByIdCache.dispose();
|
||||
this.uriPersonCache.dispose();
|
||||
this.userProfileCache.dispose();
|
||||
this.userMutingsCache.dispose();
|
||||
this.userBlockingCache.dispose();
|
||||
this.userBlockedCache.dispose();
|
||||
this.renoteMutingsCache.dispose();
|
||||
this.userFollowingsCache.dispose();
|
||||
this.userFollowingChannelsCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ export class CaptchaService {
|
||||
}, { throwErrorWhenResponseNotOk: false });
|
||||
|
||||
if (!res.ok) {
|
||||
throw `${res.status}`;
|
||||
throw new Error(`${res.status}`);
|
||||
}
|
||||
|
||||
return await res.json() as CaptchaResponse;
|
||||
@@ -39,48 +39,48 @@ export class CaptchaService {
|
||||
@bindThis
|
||||
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw 'recaptcha-failed: no response provided';
|
||||
throw new Error('recaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
|
||||
throw `recaptcha-request-failed: ${err}`;
|
||||
throw new Error(`recaptcha-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw `recaptcha-failed: ${errorCodes}`;
|
||||
throw new Error(`recaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw 'hcaptcha-failed: no response provided';
|
||||
throw new Error('hcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
||||
throw `hcaptcha-request-failed: ${err}`;
|
||||
throw new Error(`hcaptcha-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw `hcaptcha-failed: ${errorCodes}`;
|
||||
throw new Error(`hcaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw 'turnstile-failed: no response provided';
|
||||
throw new Error('turnstile-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||
throw `turnstile-request-failed: ${err}`;
|
||||
throw new Error(`turnstile-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw `turnstile-failed: ${errorCodes}`;
|
||||
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import type { EmojisRepository, Role } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -15,8 +15,10 @@ import type { Config } from '@/config.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/server/api/stream/types.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
export class CustomEmojiService implements OnApplicationShutdown {
|
||||
private cache: MemoryKVCache<Emoji | null>;
|
||||
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||
|
||||
@@ -63,6 +65,9 @@ export class CustomEmojiService {
|
||||
aliases: string[];
|
||||
host: string | null;
|
||||
license: string | null;
|
||||
isSensitive: boolean;
|
||||
localOnly: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][];
|
||||
}): Promise<Emoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
@@ -75,6 +80,9 @@ export class CustomEmojiService {
|
||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (data.host == null) {
|
||||
@@ -90,10 +98,14 @@ export class CustomEmojiService {
|
||||
|
||||
@bindThis
|
||||
public async update(id: Emoji['id'], data: {
|
||||
driveFile?: DriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][];
|
||||
}): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||
@@ -105,6 +117,12 @@ export class CustomEmojiService {
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
||||
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
|
||||
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
@@ -259,7 +277,7 @@ export class CustomEmojiService {
|
||||
|
||||
@bindThis
|
||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
const match = emojiName.match(parseEmojiStrRegexp);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
const name = match[1];
|
||||
@@ -331,4 +349,14 @@ export class CustomEmojiService {
|
||||
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.cache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
@@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class FederatedInstanceService {
|
||||
export class FederatedInstanceService implements OnApplicationShutdown {
|
||||
public federatedInstanceCache: RedisKVCache<Instance | null>;
|
||||
|
||||
constructor(
|
||||
@@ -77,4 +77,14 @@ export class FederatedInstanceService {
|
||||
|
||||
this.federatedInstanceCache.set(result.host, result);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.federatedInstanceCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -116,14 +116,14 @@ export class FetchInstanceMetadataService {
|
||||
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||
.catch(err => {
|
||||
if (err.statusCode === 404) {
|
||||
throw 'No nodeinfo provided';
|
||||
throw new Error('No nodeinfo provided');
|
||||
} else {
|
||||
throw err.statusCode ?? err.message;
|
||||
}
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||
throw 'No wellknown links';
|
||||
throw new Error('No wellknown links');
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
@@ -134,7 +134,7 @@ export class FetchInstanceMetadataService {
|
||||
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
||||
|
||||
if (link == null) {
|
||||
throw 'No nodeinfo link provided';
|
||||
throw new Error('No nodeinfo link provided');
|
||||
}
|
||||
|
||||
const info = await this.httpRequestService.getJson(link.href)
|
||||
|
@@ -120,8 +120,13 @@ export class MetaService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.intervalId);
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ export class MfmService {
|
||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
const part = txt.split('@');
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
|
@@ -510,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
if (data.poll && data.poll.expiresAt) {
|
||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||
this.queueService.endedPollNotificationQueue.add({
|
||||
this.queueService.endedPollNotificationQueue.add(note.id, {
|
||||
noteId: note.id,
|
||||
}, {
|
||||
delay,
|
||||
@@ -790,7 +790,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
||||
onApplicationShutdown(signal?: string | undefined) {
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -122,7 +122,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
onApplicationShutdown(signal?: string | undefined): void {
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -152,7 +152,13 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
*/
|
||||
}
|
||||
|
||||
onApplicationShutdown(signal?: string | undefined): void {
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import push from 'web-push';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PushNotificationService {
|
||||
export class PushNotificationService implements OnApplicationShutdown {
|
||||
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
|
||||
|
||||
constructor(
|
||||
@@ -115,4 +115,14 @@ export class PushNotificationService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.subscriptionsCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -208,7 +208,7 @@ export class QueryService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void {
|
||||
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
@@ -217,7 +217,7 @@ export class QueryService {
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}));
|
||||
}));
|
||||
} else if (!me.showTimelineReplies) {
|
||||
} else if (!withReplies) {
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
||||
|
@@ -1,42 +1,11 @@
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Bull from 'bull';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
|
||||
|
||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
redis: {
|
||||
port: config.redisForJobQueue.port,
|
||||
host: config.redisForJobQueue.host,
|
||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||
password: config.redisForJobQueue.pass,
|
||||
db: config.redisForJobQueue.db ?? 0,
|
||||
},
|
||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
|
||||
limiter: limitPerSec > 0 ? {
|
||||
max: limitPerSec,
|
||||
duration: 1000,
|
||||
} : undefined,
|
||||
settings: {
|
||||
backoffStrategies: {
|
||||
apBackoff,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||
function apBackoff(attemptsMade: number, err: Error) {
|
||||
const baseDelay = 60 * 1000; // 1min
|
||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||
backoff = Math.min(backoff, maxBackoff);
|
||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||
return backoff;
|
||||
}
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
@@ -49,49 +18,49 @@ export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
||||
|
||||
const $system: Provider = {
|
||||
provide: 'queue:system',
|
||||
useFactory: (config: Config) => q(config, 'system'),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $endedPollNotification: Provider = {
|
||||
provide: 'queue:endedPollNotification',
|
||||
useFactory: (config: Config) => q(config, 'endedPollNotification'),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $deliver: Provider = {
|
||||
provide: 'queue:deliver',
|
||||
useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $inbox: Provider = {
|
||||
provide: 'queue:inbox',
|
||||
useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $db: Provider = {
|
||||
provide: 'queue:db',
|
||||
useFactory: (config: Config) => q(config, 'db'),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $relationship: Provider = {
|
||||
provide: 'queue:relationship',
|
||||
useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $objectStorage: Provider = {
|
||||
provide: 'queue:objectStorage',
|
||||
useFactory: (config: Config) => q(config, 'objectStorage'),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $webhookDeliver: Provider = {
|
||||
provide: 'queue:webhookDeliver',
|
||||
useFactory: (config: Config) => q(config, 'webhookDeliver', 64),
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
@@ -131,7 +100,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
) {}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
@@ -151,4 +120,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
this.webhookDeliverQueue.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Bull from 'bull';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||
@@ -11,6 +10,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
@@ -26,7 +26,43 @@ export class QueueService {
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
) {}
|
||||
) {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('aggregateRetention', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
|
||||
@@ -42,11 +78,10 @@ export class QueueService {
|
||||
isSharedInbox,
|
||||
};
|
||||
|
||||
return this.deliverQueue.add(data, {
|
||||
return this.deliverQueue.add(to, data, {
|
||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -60,11 +95,10 @@ export class QueueService {
|
||||
signature,
|
||||
};
|
||||
|
||||
return this.inboxQueue.add(data, {
|
||||
return this.inboxQueue.add('', data, {
|
||||
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
||||
timeout: 5 * 60 * 1000, // 5min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -212,7 +246,7 @@ export class QueueService {
|
||||
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
|
||||
name: string,
|
||||
data: D,
|
||||
opts: Bull.JobOptions,
|
||||
opts: Bull.JobsOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
@@ -299,10 +333,10 @@ export class QueueService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): {
|
||||
name: string,
|
||||
data: RelationshipJobData,
|
||||
opts: Bull.JobOptions,
|
||||
opts: Bull.JobsOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
@@ -351,11 +385,10 @@ export class QueueService {
|
||||
eventId: uuid(),
|
||||
};
|
||||
|
||||
return this.webhookDeliverQueue.add(data, {
|
||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -367,11 +400,11 @@ export class QueueService {
|
||||
this.deliverQueue.once('cleaned', (jobs, status) => {
|
||||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
this.deliverQueue.clean(0, 'delayed');
|
||||
this.deliverQueue.clean(0, Infinity, 'delayed');
|
||||
|
||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
this.inboxQueue.clean(0, 'delayed');
|
||||
this.inboxQueue.clean(0, Infinity, 'delayed');
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
@@ -54,6 +55,9 @@ type DecodedReaction = {
|
||||
host?: string | null;
|
||||
};
|
||||
|
||||
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
|
||||
const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||
|
||||
@Injectable()
|
||||
export class ReactionService {
|
||||
constructor(
|
||||
@@ -72,6 +76,7 @@ export class ReactionService {
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
@@ -85,7 +90,7 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) {
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) {
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
@@ -99,10 +104,41 @@ export class ReactionService {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else {
|
||||
reaction = await this.toDbReaction(reaction, user.host);
|
||||
} else if (_reaction) {
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||
|
||||
const name = custom[1];
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) {
|
||||
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
||||
reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
|
||||
// センシティブ
|
||||
if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
// リアクションとして使う権限がない
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
reaction = this.normalize(reaction ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
const record: NoteReaction = {
|
||||
@@ -288,11 +324,9 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
||||
public normalize(reaction: string | null): string {
|
||||
if (reaction == null) return FALLBACK;
|
||||
|
||||
reacterHost = this.utilityService.toPunyNullable(reacterHost);
|
||||
|
||||
// 文字列タイプのリアクションを絵文字に変換
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
|
||||
@@ -306,25 +340,12 @@ export class ReactionService {
|
||||
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||
}
|
||||
|
||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
}
|
||||
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public decodeReaction(str: string): DecodedReaction {
|
||||
const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
|
||||
const custom = str.match(decodeCustomEmojiRegexp);
|
||||
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
|
@@ -306,6 +306,14 @@ export class RoleService implements OnApplicationShutdown {
|
||||
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isExplorable(role: { id: Role['id']} | null): Promise<boolean> {
|
||||
if (role == null) return false;
|
||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||
if (check == null) return false;
|
||||
return check.isExplorable;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
@@ -425,7 +433,13 @@ export class RoleService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
this.roleAssignmentByUserIdCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { UserKeypairsRepository } from '@/models/index.js';
|
||||
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserKeypairService {
|
||||
export class UserKeypairService implements OnApplicationShutdown {
|
||||
private cache: RedisKVCache<UserKeypair>;
|
||||
|
||||
constructor(
|
||||
@@ -31,4 +31,14 @@ export class UserKeypairService {
|
||||
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||
return await this.cache.fetch(userId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.cache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,9 @@ type IWebFinger = {
|
||||
subject: string;
|
||||
};
|
||||
|
||||
const urlRegex = /^https?:\/\//;
|
||||
const mRegex = /^([^@]+)@(.*)/;
|
||||
|
||||
@Injectable()
|
||||
export class WebfingerService {
|
||||
constructor(
|
||||
@@ -35,12 +38,12 @@ export class WebfingerService {
|
||||
|
||||
@bindThis
|
||||
private genUrl(query: string): string {
|
||||
if (query.match(/^https?:\/\//)) {
|
||||
if (query.match(urlRegex)) {
|
||||
const u = new URL(query);
|
||||
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
||||
}
|
||||
|
||||
const m = query.match(/^([^@]+)@(.*)/);
|
||||
const m = query.match(mRegex);
|
||||
if (m) {
|
||||
const hostname = m[2];
|
||||
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
|
||||
|
@@ -81,7 +81,12 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import escapeRegexp from 'escape-regexp';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
@@ -30,7 +30,7 @@ export type UriParseResult = {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ApDbResolverService {
|
||||
export class ApDbResolverService implements OnApplicationShutdown {
|
||||
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
||||
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
||||
|
||||
@@ -162,4 +162,15 @@ export class ApDbResolverService {
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.publicKeyCache.dispose();
|
||||
this.publicKeyByUserIdCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -277,7 +277,7 @@ export class ApRendererService {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||
if (emoji && !emoji.localOnly) object.tag = [this.renderEmoji(emoji)];
|
||||
}
|
||||
|
||||
return object;
|
||||
@@ -400,7 +400,7 @@ export class ApRendererService {
|
||||
}));
|
||||
|
||||
const emojis = await this.getEmojis(note.emojis);
|
||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
const tag = [
|
||||
...hashtagTags,
|
||||
@@ -479,7 +479,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
const emojis = await this.getEmojis(user.emojis);
|
||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||
|
||||
|
@@ -94,7 +94,7 @@ class LdSignature {
|
||||
@bindThis
|
||||
private getLoader() {
|
||||
return async (url: string): Promise<any> => {
|
||||
if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
|
||||
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
|
||||
|
||||
if (this.preLoad) {
|
||||
if (url in CONTEXTS) {
|
||||
@@ -126,7 +126,7 @@ class LdSignature {
|
||||
timeout: this.loderTimeout,
|
||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
throw new Error(`${res.status} ${res.statusText}`);
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import { PollService } from '@/core/PollService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
@@ -32,7 +33,6 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||
import { ApImageService } from './ApImageService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IPost } from '../type.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApNoteService {
|
||||
@@ -230,7 +230,7 @@ export class ApNoteService {
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw 'quote resolve failed';
|
||||
throw new Error('quote resolve failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +311,7 @@ export class ApNoteService {
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 };
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
@@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, IObject } from '../type.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
@@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit {
|
||||
tags,
|
||||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
showTimelineReplies: false,
|
||||
})) as RemoteUser;
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
@@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
||||
return 'skip: dst.alsoKnownAs is empty';
|
||||
}
|
||||
if (!dst.alsoKnownAs?.includes(src.uri)) {
|
||||
if (!dst.alsoKnownAs.includes(src.uri)) {
|
||||
return 'skip: alsoKnownAs does not include from.uri';
|
||||
}
|
||||
|
||||
|
@@ -60,7 +60,8 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||
}, 1000 * 60 * 20);
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
clearInterval(this.saveIntervalId);
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
await Promise.all(
|
||||
@@ -68,4 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,8 @@ export class EmojiEntityService {
|
||||
category: emoji.category,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
isSensitive: emoji.isSensitive ? true : undefined,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +53,9 @@ export class EmojiEntityService {
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
||||
achievements: profile!.achievements,
|
||||
loggedInDays: profile!.loggedInDates.length,
|
||||
policies: this.roleService.getUserPolicies(user.id),
|
||||
|
@@ -35,6 +35,7 @@ export class UserListEntityService {
|
||||
createdAt: userList.createdAt.toISOString(),
|
||||
name: userList.name,
|
||||
userIds: users.map(x => x.userId),
|
||||
isPublic: userList.isPublic,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,12 @@ export class JanitorService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Xev from 'xev';
|
||||
import * as Bull from 'bullmq';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const ev = new Xev();
|
||||
@@ -13,6 +17,9 @@ export class QueueStatsService implements OnApplicationShutdown {
|
||||
private intervalId: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
@@ -31,11 +38,14 @@ export class QueueStatsService implements OnApplicationShutdown {
|
||||
let activeDeliverJobs = 0;
|
||||
let activeInboxJobs = 0;
|
||||
|
||||
this.queueService.deliverQueue.on('global:active', () => {
|
||||
const deliverQueueEvents = new Bull.QueueEvents(QUEUE.DELIVER, baseQueueOptions(this.config, QUEUE.DELIVER));
|
||||
const inboxQueueEvents = new Bull.QueueEvents(QUEUE.INBOX, baseQueueOptions(this.config, QUEUE.INBOX));
|
||||
|
||||
deliverQueueEvents.on('active', () => {
|
||||
activeDeliverJobs++;
|
||||
});
|
||||
|
||||
this.queueService.inboxQueue.on('global:active', () => {
|
||||
inboxQueueEvents.on('active', () => {
|
||||
activeInboxJobs++;
|
||||
});
|
||||
|
||||
@@ -71,9 +81,14 @@ export class QueueStatsService implements OnApplicationShutdown {
|
||||
|
||||
this.intervalId = setInterval(tick, interval);
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -63,9 +63,14 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// CPU STAT
|
||||
|
@@ -25,6 +25,7 @@ export const DI = {
|
||||
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
|
||||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||
userListsRepository: Symbol('userListsRepository'),
|
||||
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||
userIpsRepository: Symbol('userIpsRepository'),
|
||||
|
@@ -83,6 +83,16 @@ export class RedisKVCache<T> {
|
||||
|
||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public gc() {
|
||||
this.memoryCache.gc();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
this.memoryCache.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class RedisSingleCache<T> {
|
||||
@@ -174,10 +184,15 @@ export class RedisSingleCache<T> {
|
||||
export class MemoryKVCache<T> {
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
private gcIntervalHandle: NodeJS.Timer;
|
||||
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
|
||||
this.gcIntervalHandle = setInterval(() => {
|
||||
this.gc();
|
||||
}, 1000 * 60 * 3);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -200,7 +215,7 @@ export class MemoryKVCache<T> {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public delete(key: string) {
|
||||
public delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
@@ -255,6 +270,21 @@ export class MemoryKVCache<T> {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public gc(): void {
|
||||
const now = Date.now();
|
||||
for (const [key, { date }] of this.cache.entries()) {
|
||||
if ((now - date) > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
clearInterval(this.gcIntervalHandle);
|
||||
}
|
||||
}
|
||||
|
||||
export class MemorySingleCache<T> {
|
||||
|
@@ -21,7 +21,7 @@ function getNoise(): string {
|
||||
|
||||
export function genAid(date: Date): string {
|
||||
const t = date.getTime();
|
||||
if (isNaN(t)) throw 'Failed to create AID: Invalid Date';
|
||||
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + getNoise();
|
||||
}
|
||||
|
@@ -5,15 +5,16 @@ const dateTimeIntervals = {
|
||||
};
|
||||
|
||||
export function dateUTC(time: number[]): Date {
|
||||
const d = time.length === 2 ? Date.UTC(time[0], time[1])
|
||||
: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
|
||||
: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
|
||||
: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
|
||||
: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
|
||||
: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
|
||||
: null;
|
||||
const d =
|
||||
time.length === 2 ? Date.UTC(time[0], time[1])
|
||||
: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
|
||||
: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
|
||||
: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
|
||||
: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
|
||||
: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
|
||||
: null;
|
||||
|
||||
if (!d) throw 'wrong number of arguments';
|
||||
if (!d) throw new Error('wrong number of arguments');
|
||||
|
||||
return new Date(d);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js';
|
||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -112,6 +112,12 @@ const $userListsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userListFavoritesRepository: Provider = {
|
||||
provide: DI.userListFavoritesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(UserListFavorite),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userListJoiningsRepository: Provider = {
|
||||
provide: DI.userListJoiningsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(UserListJoining),
|
||||
@@ -416,6 +422,7 @@ const $userMemosRepository: Provider = {
|
||||
$userSecurityKeysRepository,
|
||||
$userPublickeysRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListJoiningsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userIpsRepository,
|
||||
@@ -483,6 +490,7 @@ const $userMemosRepository: Provider = {
|
||||
$userSecurityKeysRepository,
|
||||
$userPublickeysRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListJoiningsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userIpsRepository,
|
||||
|
@@ -60,4 +60,20 @@ export class Emoji {
|
||||
length: 1024, nullable: true,
|
||||
})
|
||||
public license: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public localOnly: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
|
||||
@Column('varchar', {
|
||||
array: true, length: 128, default: '{}',
|
||||
})
|
||||
public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
|
||||
}
|
||||
|
@@ -101,13 +101,25 @@ export class Meta {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public errorImageUrl: string | null;
|
||||
public iconUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public iconUrl: string | null;
|
||||
public serverErrorImageUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public notFoundImageUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public infoImageUrl: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
|
@@ -90,7 +90,7 @@ export class Note {
|
||||
@Column('varchar', {
|
||||
length: 64, nullable: true,
|
||||
})
|
||||
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null;
|
||||
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
|
||||
|
||||
@Column('smallint', {
|
||||
default: 0,
|
||||
|
@@ -232,12 +232,6 @@ export class User {
|
||||
})
|
||||
public followersUri: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether to show users replying to other users in the timeline.',
|
||||
})
|
||||
public showTimelineReplies: boolean;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('char', {
|
||||
length: 16, nullable: true, unique: true,
|
||||
|
@@ -19,6 +19,12 @@ export class UserList {
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isPublic: boolean;
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
33
packages/backend/src/models/entities/UserListFavorite.ts
Normal file
33
packages/backend/src/models/entities/UserListFavorite.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './User.js';
|
||||
import { UserList } from './UserList.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'userListId'], { unique: true })
|
||||
export class UserListFavorite {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column(id())
|
||||
public userListId: UserList['id'];
|
||||
|
||||
@ManyToOne(type => UserList, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public userList: UserList | null;
|
||||
}
|
@@ -49,6 +49,7 @@ import { User } from '@/models/entities/User.js';
|
||||
import { UserIp } from '@/models/entities/UserIp.js';
|
||||
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import { UserList } from '@/models/entities/UserList.js';
|
||||
import { UserListFavorite } from './entities/UserListFavorite.js';
|
||||
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
||||
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||
import { UserPending } from '@/models/entities/UserPending.js';
|
||||
@@ -117,6 +118,7 @@ export {
|
||||
UserIp,
|
||||
UserKeypair,
|
||||
UserList,
|
||||
UserListFavorite,
|
||||
UserListJoining,
|
||||
UserNotePining,
|
||||
UserPending,
|
||||
@@ -184,6 +186,7 @@ export type UsersRepository = Repository<User>;
|
||||
export type UserIpsRepository = Repository<UserIp>;
|
||||
export type UserKeypairsRepository = Repository<UserKeypair>;
|
||||
export type UserListsRepository = Repository<UserList>;
|
||||
export type UserListFavoritesRepository = Repository<UserListFavorite>;
|
||||
export type UserListJoiningsRepository = Repository<UserListJoining>;
|
||||
export type UserNotePiningsRepository = Repository<UserNotePining>;
|
||||
export type UserPendingsRepository = Repository<UserPending>;
|
||||
|
@@ -22,6 +22,19 @@ export const packedEmojiSimpleSchema = {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -63,5 +76,22 @@ export const packedEmojiDetailedSchema = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localOnly: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -25,5 +25,10 @@ export const packedUserListSchema = {
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
nullable: false,
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -57,6 +57,7 @@ import { User } from '@/models/entities/User.js';
|
||||
import { UserIp } from '@/models/entities/UserIp.js';
|
||||
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import { UserList } from '@/models/entities/UserList.js';
|
||||
import { UserListFavorite } from '@/models/entities/UserListFavorite.js';
|
||||
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
||||
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||
import { UserPending } from '@/models/entities/UserPending.js';
|
||||
@@ -132,6 +133,7 @@ export const entities = [
|
||||
UserKeypair,
|
||||
UserPublickey,
|
||||
UserList,
|
||||
UserListFavorite,
|
||||
UserListJoining,
|
||||
UserNotePining,
|
||||
UserSecurityKey,
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { getJobInfo } from './get-job-info.js';
|
||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
@@ -35,17 +34,51 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QUEUE, baseQueueOptions } from './const.js';
|
||||
|
||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||
function httpRelatedBackoff(attemptsMade: number) {
|
||||
const baseDelay = 60 * 1000; // 1min
|
||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||
backoff = Math.min(backoff, maxBackoff);
|
||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||
return backoff;
|
||||
}
|
||||
|
||||
function getJobInfo(job: Bull.Job | undefined, increment = false): string {
|
||||
if (job == null) return '-';
|
||||
|
||||
const age = Date.now() - job.timestamp;
|
||||
|
||||
const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
|
||||
: age > 10000 ? `${Math.floor(age / 1000)}s`
|
||||
: `${age}ms`;
|
||||
|
||||
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
|
||||
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
||||
const maxAttempts = job.opts ? job.opts.attempts : 0;
|
||||
|
||||
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class QueueProcessorService {
|
||||
export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private systemQueueWorker: Bull.Worker;
|
||||
private dbQueueWorker: Bull.Worker;
|
||||
private deliverQueueWorker: Bull.Worker;
|
||||
private inboxQueueWorker: Bull.Worker;
|
||||
private webhookDeliverQueueWorker: Bull.Worker;
|
||||
private relationshipQueueWorker: Bull.Worker;
|
||||
private objectStorageQueueWorker: Bull.Worker;
|
||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private queueService: QueueService,
|
||||
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
@@ -77,10 +110,7 @@ export class QueueProcessorService {
|
||||
private cleanProcessorService: CleanProcessorService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public start() {
|
||||
function renderError(e: Error): any {
|
||||
if (e) { // 何故かeがundefinedで来ることがある
|
||||
return {
|
||||
@@ -97,146 +127,232 @@ export class QueueProcessorService {
|
||||
}
|
||||
}
|
||||
|
||||
const systemLogger = this.logger.createSubLogger('system');
|
||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
||||
const dbLogger = this.logger.createSubLogger('db');
|
||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
||||
//#region system
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
switch (job.name) {
|
||||
case 'tickCharts': return this.tickChartsProcessorService.process();
|
||||
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
||||
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||
case 'clean': return this.cleanProcessorService.process();
|
||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.SYSTEM),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
this.queueService.systemQueue
|
||||
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
||||
const systemLogger = this.logger.createSubLogger('system');
|
||||
|
||||
this.systemQueueWorker
|
||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
this.queueService.deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
//#region db
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
switch (job.name) {
|
||||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||
case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
|
||||
case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
|
||||
case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
|
||||
case 'importFollowing': return this.importFollowingProcessorService.process(job);
|
||||
case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
|
||||
case 'importMuting': return this.importMutingProcessorService.process(job);
|
||||
case 'importBlocking': return this.importBlockingProcessorService.process(job);
|
||||
case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
|
||||
case 'importUserLists': return this.importUserListsProcessorService.process(job);
|
||||
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
||||
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.DB),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
this.queueService.inboxQueue
|
||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
|
||||
const dbLogger = this.logger.createSubLogger('db');
|
||||
|
||||
this.queueService.dbQueue
|
||||
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
|
||||
this.dbQueueWorker
|
||||
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
|
||||
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
this.queueService.relationshipQueue
|
||||
.on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`));
|
||||
//#region deliver
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
|
||||
...baseQueueOptions(this.config, QUEUE.DELIVER),
|
||||
autorun: false,
|
||||
concurrency: this.config.deliverJobConcurrency ?? 128,
|
||||
limiter: {
|
||||
max: this.config.deliverJobPerSec ?? 128,
|
||||
duration: 1000,
|
||||
},
|
||||
settings: {
|
||||
backoffStrategy: httpRelatedBackoff,
|
||||
},
|
||||
});
|
||||
|
||||
this.queueService.objectStorageQueue
|
||||
.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
||||
|
||||
this.queueService.webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
this.deliverQueueWorker
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
||||
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
//#region inbox
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
|
||||
...baseQueueOptions(this.config, QUEUE.INBOX),
|
||||
autorun: false,
|
||||
concurrency: this.config.inboxJobConcurrency ?? 16,
|
||||
limiter: {
|
||||
max: this.config.inboxJobPerSec ?? 16,
|
||||
duration: 1000,
|
||||
},
|
||||
settings: {
|
||||
backoffStrategy: httpRelatedBackoff,
|
||||
},
|
||||
});
|
||||
|
||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
||||
|
||||
this.inboxQueueWorker
|
||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
|
||||
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
//#region webhook deliver
|
||||
this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
|
||||
...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
concurrency: 64,
|
||||
limiter: {
|
||||
max: 64,
|
||||
duration: 1000,
|
||||
},
|
||||
settings: {
|
||||
backoffStrategy: httpRelatedBackoff,
|
||||
},
|
||||
});
|
||||
|
||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
||||
|
||||
this.webhookDeliverQueueWorker
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
||||
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
this.queueService.systemQueue.add('tickCharts', {
|
||||
//#region relationship
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
switch (job.name) {
|
||||
case 'follow': return this.relationshipProcessorService.processFollow(job);
|
||||
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
|
||||
case 'block': return this.relationshipProcessorService.processBlock(job);
|
||||
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
||||
}
|
||||
}, {
|
||||
repeat: { cron: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
autorun: false,
|
||||
concurrency: this.config.relashionshipJobConcurrency ?? 16,
|
||||
limiter: {
|
||||
max: this.config.relashionshipJobPerSec ?? 64,
|
||||
duration: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
this.queueService.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.queueService.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.queueService.systemQueue.add('aggregateRetention', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.queueService.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.queueService.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { cron: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
|
||||
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
|
||||
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
|
||||
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
|
||||
|
||||
this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
|
||||
this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
|
||||
this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
|
||||
this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
|
||||
|
||||
this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job));
|
||||
this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done));
|
||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
||||
|
||||
{
|
||||
const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
|
||||
this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
|
||||
this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
|
||||
this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
|
||||
this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
|
||||
}
|
||||
this.relationshipQueueWorker
|
||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done));
|
||||
this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done));
|
||||
this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done));
|
||||
this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done));
|
||||
this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done));
|
||||
this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done));
|
||||
//#region object storage
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
switch (job.name) {
|
||||
case 'deleteFile': return this.deleteFileProcessorService.process(job);
|
||||
case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
|
||||
}
|
||||
}, {
|
||||
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
autorun: false,
|
||||
concurrency: 16,
|
||||
});
|
||||
|
||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
this.objectStorageQueueWorker
|
||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
|
||||
//#endregion
|
||||
|
||||
//#region ended poll notification
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
|
||||
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
autorun: false,
|
||||
});
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async start(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.systemQueueWorker.run(),
|
||||
this.dbQueueWorker.run(),
|
||||
this.deliverQueueWorker.run(),
|
||||
this.inboxQueueWorker.run(),
|
||||
this.webhookDeliverQueueWorker.run(),
|
||||
this.relationshipQueueWorker.run(),
|
||||
this.objectStorageQueueWorker.run(),
|
||||
this.endedPollNotificationQueueWorker.run(),
|
||||
]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async stop(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.systemQueueWorker.close(),
|
||||
this.dbQueueWorker.close(),
|
||||
this.deliverQueueWorker.close(),
|
||||
this.inboxQueueWorker.close(),
|
||||
this.webhookDeliverQueueWorker.close(),
|
||||
this.relationshipQueueWorker.close(),
|
||||
this.objectStorageQueueWorker.close(),
|
||||
this.endedPollNotificationQueueWorker.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||
await this.stop();
|
||||
}
|
||||
}
|
||||
|
26
packages/backend/src/queue/const.ts
Normal file
26
packages/backend/src/queue/const.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Config } from '@/config.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
export const QUEUE = {
|
||||
DELIVER: 'deliver',
|
||||
INBOX: 'inbox',
|
||||
SYSTEM: 'system',
|
||||
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||
DB: 'db',
|
||||
RELATIONSHIP: 'relationship',
|
||||
OBJECT_STORAGE: 'objectStorage',
|
||||
WEBHOOK_DELIVER: 'webhookDeliver',
|
||||
};
|
||||
|
||||
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
||||
return {
|
||||
connection: {
|
||||
port: config.redisForJobQueue.port,
|
||||
host: config.redisForJobQueue.host,
|
||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||
password: config.redisForJobQueue.pass,
|
||||
db: config.redisForJobQueue.db ?? 0,
|
||||
},
|
||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
|
||||
};
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
import Bull from 'bull';
|
||||
|
||||
export function getJobInfo(job: Bull.Job, increment = false) {
|
||||
const age = Date.now() - job.timestamp;
|
||||
|
||||
const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
|
||||
: age > 10000 ? `${Math.floor(age / 1000)}s`
|
||||
: `${age}ms`;
|
||||
|
||||
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
|
||||
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
||||
const maxAttempts = job.opts ? job.opts.attempts : 0;
|
||||
|
||||
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
|
||||
}
|
@@ -9,7 +9,7 @@ import { deepClone } from '@/misc/clone.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class AggregateRetentionProcessorService {
|
||||
@@ -32,7 +32,7 @@ export class AggregateRetentionProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Aggregating retention...');
|
||||
|
||||
const now = new Date();
|
||||
@@ -62,7 +62,6 @@ export class AggregateRetentionProcessorService {
|
||||
} catch (err) {
|
||||
if (isDuplicateKeyValueError(err)) {
|
||||
this.logger.succ('Skip because it has already been processed by another worker.');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
@@ -88,6 +87,5 @@ export class AggregateRetentionProcessorService {
|
||||
}
|
||||
|
||||
this.logger.succ('Retention aggregated.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class CheckExpiredMutingsProcessorService {
|
||||
@@ -27,7 +27,7 @@ export class CheckExpiredMutingsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Checking expired mutings...');
|
||||
|
||||
const expired = await this.mutingsRepository.createQueryBuilder('muting')
|
||||
@@ -41,6 +41,5 @@ export class CheckExpiredMutingsProcessorService {
|
||||
}
|
||||
|
||||
this.logger.succ('All expired mutings checked.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class CleanChartsProcessorService {
|
||||
@@ -45,7 +45,7 @@ export class CleanChartsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Clean charts...');
|
||||
|
||||
await Promise.all([
|
||||
@@ -64,6 +64,5 @@ export class CleanChartsProcessorService {
|
||||
]);
|
||||
|
||||
this.logger.succ('All charts successfully cleaned.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class CleanProcessorService {
|
||||
@@ -36,7 +36,7 @@ export class CleanProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Cleaning...');
|
||||
|
||||
this.userIpsRepository.delete({
|
||||
@@ -72,6 +72,5 @@ export class CleanProcessorService {
|
||||
}
|
||||
|
||||
this.logger.succ('Cleaned.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,9 @@ import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class CleanRemoteFilesProcessorService {
|
||||
@@ -27,7 +27,7 @@ export class CleanRemoteFilesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
|
||||
this.logger.info('Deleting cached remote files...');
|
||||
|
||||
let deletedCount = 0;
|
||||
@@ -47,7 +47,7 @@ export class CleanRemoteFilesProcessorService {
|
||||
});
|
||||
|
||||
if (files.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -62,10 +62,9 @@ export class CleanRemoteFilesProcessorService {
|
||||
isLink: false,
|
||||
});
|
||||
|
||||
job.progress(deletedCount / total);
|
||||
job.updateProgress(deletedCount / total);
|
||||
}
|
||||
|
||||
this.logger.succ('All cached remote files has been deleted.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -8,10 +8,10 @@ import { DriveService } from '@/core/DriveService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserDeleteJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserDeleteJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountProcessorService {
|
||||
|
@@ -5,10 +5,10 @@ import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteDriveFilesProcessorService {
|
||||
@@ -31,12 +31,11 @@ export class DeleteDriveFilesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ export class DeleteDriveFilesProcessorService {
|
||||
});
|
||||
|
||||
if (files.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -71,10 +70,9 @@ export class DeleteDriveFilesProcessorService {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.progress(deletedCount / total);
|
||||
job.updateProgress(deletedCount / total);
|
||||
}
|
||||
|
||||
this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -3,10 +3,10 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { ObjectStorageFileJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { ObjectStorageFileJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteFileProcessorService {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -16,7 +17,6 @@ import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DeliverJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -121,15 +121,13 @@ export class DeliverProcessorService {
|
||||
isSuspended: true,
|
||||
});
|
||||
});
|
||||
return `${host} is gone`;
|
||||
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||
}
|
||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
|
@@ -6,7 +6,7 @@ import type Logger from '@/logger.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { EndedPollNotificationJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -30,10 +30,9 @@ export class EndedPollNotificationProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<EndedPollNotificationJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<EndedPollNotificationJobData>): Promise<void> {
|
||||
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
|
||||
if (note == null || !note.hasPoll) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +50,5 @@ export class EndedPollNotificationProcessorService {
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type { DBExportAntennasData } from '../types.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class ExportAntennasProcessorService {
|
||||
@@ -39,10 +39,9 @@ export class ExportAntennasProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DBExportAntennasData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> {
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const [path, cleanup] = await createTemp();
|
||||
@@ -96,7 +95,6 @@ export class ExportAntennasProcessorService {
|
||||
this.logger.succ('Exported to: ' + driveFile.id);
|
||||
} finally {
|
||||
cleanup();
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportBlockingProcessorService {
|
||||
@@ -36,12 +36,11 @@ export class ExportBlockingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,7 +68,7 @@ export class ExportBlockingProcessorService {
|
||||
});
|
||||
|
||||
if (blockings.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -99,7 +98,7 @@ export class ExportBlockingProcessorService {
|
||||
blockerId: user.id,
|
||||
});
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
job.updateProgress(exportedCount / total);
|
||||
}
|
||||
|
||||
stream.end();
|
||||
@@ -112,7 +111,5 @@ export class ExportBlockingProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class ExportCustomEmojisProcessorService {
|
||||
@@ -37,12 +37,11 @@ export class ExportCustomEmojisProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job): Promise<void> {
|
||||
this.logger.info('Exporting custom emojis ...');
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,24 +116,26 @@ export class ExportCustomEmojisProcessorService {
|
||||
metaStream.end();
|
||||
|
||||
// Create archive
|
||||
const [archivePath, archiveCleanup] = await createTemp();
|
||||
const archiveStream = fs.createWriteStream(archivePath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 0 },
|
||||
});
|
||||
archiveStream.on('close', async () => {
|
||||
this.logger.succ(`Exported to: ${archivePath}`);
|
||||
await new Promise<void>(async (resolve) => {
|
||||
const [archivePath, archiveCleanup] = await createTemp();
|
||||
const archiveStream = fs.createWriteStream(archivePath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 0 },
|
||||
});
|
||||
archiveStream.on('close', async () => {
|
||||
this.logger.succ(`Exported to: ${archivePath}`);
|
||||
|
||||
const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
|
||||
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
|
||||
const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
|
||||
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
archiveCleanup();
|
||||
done();
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
cleanup();
|
||||
archiveCleanup();
|
||||
resolve();
|
||||
});
|
||||
archive.pipe(archiveStream);
|
||||
archive.directory(path, false);
|
||||
archive.finalize();
|
||||
});
|
||||
archive.pipe(archiveStream);
|
||||
archive.directory(path, false);
|
||||
archive.finalize();
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -42,12 +42,11 @@ export class ExportFavoritesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ export class ExportFavoritesProcessorService {
|
||||
}) as (NoteFavorite & { note: Note & { user: User } })[];
|
||||
|
||||
if (favorites.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ export class ExportFavoritesProcessorService {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.progress(exportedFavoritesCount / total);
|
||||
job.updateProgress(exportedFavoritesCount / total);
|
||||
}
|
||||
|
||||
await write(']');
|
||||
@@ -127,8 +126,6 @@ export class ExportFavoritesProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import type { Following } from '@/models/entities/Following.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbExportFollowingData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbExportFollowingData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportFollowingProcessorService {
|
||||
@@ -40,12 +40,11 @@ export class ExportFollowingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbExportFollowingData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> {
|
||||
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +115,5 @@ export class ExportFollowingProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportMutingProcessorService {
|
||||
@@ -39,12 +39,11 @@ export class ExportMutingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ export class ExportMutingProcessorService {
|
||||
});
|
||||
|
||||
if (mutes.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,7 +102,7 @@ export class ExportMutingProcessorService {
|
||||
muterId: user.id,
|
||||
});
|
||||
|
||||
job.progress(exportedCount / total);
|
||||
job.updateProgress(exportedCount / total);
|
||||
}
|
||||
|
||||
stream.end();
|
||||
@@ -116,7 +115,5 @@ export class ExportMutingProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -39,12 +39,11 @@ export class ExportNotesProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ export class ExportNotesProcessorService {
|
||||
}) as Note[];
|
||||
|
||||
if (notes.length === 0) {
|
||||
job.progress(100);
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -108,7 +107,7 @@ export class ExportNotesProcessorService {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.progress(exportedNotesCount / total);
|
||||
job.updateProgress(exportedNotesCount / total);
|
||||
}
|
||||
|
||||
await write(']');
|
||||
@@ -123,8 +122,6 @@ export class ExportNotesProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportUserListsProcessorService {
|
||||
@@ -39,12 +39,11 @@ export class ExportUserListsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +91,5 @@ export class ExportUserListsProcessorService {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import { DBAntennaImportJobData } from '../types.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
const validate = new Ajv().compile({
|
||||
type: 'object',
|
||||
@@ -59,7 +59,7 @@ export class ImportAntennasProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DBAntennaImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> {
|
||||
const now = new Date();
|
||||
try {
|
||||
for (const antenna of job.data.antenna) {
|
||||
@@ -89,8 +89,6 @@ export class ImportAntennasProcessorService {
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(err);
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportBlockingProcessorService {
|
||||
@@ -34,12 +34,11 @@ export class ImportBlockingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||
this.logger.info(`Importing blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +46,6 @@ export class ImportBlockingProcessorService {
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +54,6 @@ export class ImportBlockingProcessorService {
|
||||
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
||||
|
||||
this.logger.succ('Import jobs created');
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -85,7 +82,7 @@ export class ImportBlockingProcessorService {
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `Unable to resolve user: @${username}@${host}`;
|
||||
throw new Error(`Unable to resolve user: @${username}@${host}`);
|
||||
}
|
||||
|
||||
// skip myself
|
||||
|
@@ -12,7 +12,7 @@ import { DriveService } from '@/core/DriveService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
|
||||
// TODO: 名前衝突時の動作を選べるようにする
|
||||
@@ -45,14 +45,13 @@ export class ImportCustomEmojisProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||
this.logger.info('Importing custom emojis ...');
|
||||
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,13 +106,15 @@ export class ImportCustomEmojisProcessorService {
|
||||
aliases: emojiInfo.aliases,
|
||||
driveFile,
|
||||
license: emojiInfo.license,
|
||||
isSensitive: emojiInfo.isSensitive,
|
||||
localOnly: emojiInfo.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||
});
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
this.logger.succ('Imported');
|
||||
done();
|
||||
});
|
||||
unzipStream.pipe(extractor);
|
||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
||||
|
@@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportFollowingProcessorService {
|
||||
@@ -34,12 +34,11 @@ export class ImportFollowingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||
this.logger.info(`Importing following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +46,6 @@ export class ImportFollowingProcessorService {
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +54,6 @@ export class ImportFollowingProcessorService {
|
||||
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
|
||||
|
||||
this.logger.succ('Import jobs created');
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -85,7 +82,7 @@ export class ImportFollowingProcessorService {
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `Unable to resolve user: @${username}@${host}`;
|
||||
throw new Error(`Unable to resolve user: @${username}@${host}`);
|
||||
}
|
||||
|
||||
// skip myself
|
||||
|
@@ -9,10 +9,10 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ImportMutingProcessorService {
|
||||
@@ -38,12 +38,11 @@ export class ImportMutingProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +50,6 @@ export class ImportMutingProcessorService {
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,7 +81,7 @@ export class ImportMutingProcessorService {
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
throw `cannot resolve user: @${username}@${host}`;
|
||||
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||
}
|
||||
|
||||
// skip myself
|
||||
@@ -98,6 +96,5 @@ export class ImportMutingProcessorService {
|
||||
}
|
||||
|
||||
this.logger.succ('Imported');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserImportJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -46,12 +46,11 @@ export class ImportUserListsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,7 +58,6 @@ export class ImportUserListsProcessorService {
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,6 +107,5 @@ export class ImportUserListsProcessorService {
|
||||
}
|
||||
|
||||
this.logger.succ('Imported');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
@@ -23,10 +23,8 @@ import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
|
||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { InboxJobData } from '../types.js';
|
||||
|
||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
||||
@Injectable()
|
||||
export class InboxProcessorService {
|
||||
private logger: Logger;
|
||||
@@ -35,12 +33,6 @@ export class InboxProcessorService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private apInboxService: ApInboxService,
|
||||
@@ -93,24 +85,24 @@ export class InboxProcessorService {
|
||||
try {
|
||||
authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor));
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (err.isClientError) {
|
||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`;
|
||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||
}
|
||||
throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`;
|
||||
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// それでもわからなければ終了
|
||||
if (authUser == null) {
|
||||
return 'skip: failed to resolve user';
|
||||
throw new Bull.UnrecoverableError('skip: failed to resolve user');
|
||||
}
|
||||
|
||||
// publicKey がなくても終了
|
||||
if (authUser.key == null) {
|
||||
return 'skip: failed to resolve user publicKey';
|
||||
throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
|
||||
}
|
||||
|
||||
// HTTP-Signatureの検証
|
||||
@@ -118,10 +110,10 @@ export class InboxProcessorService {
|
||||
|
||||
// また、signatureのsignerは、activity.actorと一致する必要がある
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||
if (activity.signature) {
|
||||
if (activity.signature.type !== 'RsaSignature2017') {
|
||||
return `skip: unsupported LD-signature type ${activity.signature.type}`;
|
||||
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
|
||||
}
|
||||
|
||||
// activity.signature.creator: https://example.oom/users/user#main-key
|
||||
@@ -134,32 +126,32 @@ export class InboxProcessorService {
|
||||
// keyIdからLD-Signatureのユーザーを取得
|
||||
authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
|
||||
if (authUser == null) {
|
||||
return 'skip: LD-Signatureのユーザーが取得できませんでした';
|
||||
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
|
||||
}
|
||||
|
||||
if (authUser.key == null) {
|
||||
return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした';
|
||||
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
|
||||
}
|
||||
|
||||
// LD-Signature検証
|
||||
const ldSignature = this.ldSignatureService.use();
|
||||
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||
if (!verified) {
|
||||
return 'skip: LD-Signatureの検証に失敗しました';
|
||||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||
}
|
||||
|
||||
// もう一度actorチェック
|
||||
if (authUser.user.uri !== activity.actor) {
|
||||
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
||||
}
|
||||
|
||||
// ブロックしてたら中断
|
||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
||||
return `Blocked request: ${ldHost}`;
|
||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||
}
|
||||
} else {
|
||||
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
|
||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +160,7 @@ export class InboxProcessorService {
|
||||
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
||||
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
||||
if (signerHost !== activityIdHost) {
|
||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
||||
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
@@ -15,7 +15,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class ResyncChartsProcessorService {
|
||||
@@ -43,7 +43,7 @@ export class ResyncChartsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Resync charts...');
|
||||
|
||||
// TODO: ユーザーごとのチャートも更新する
|
||||
@@ -55,6 +55,5 @@ export class ResyncChartsProcessorService {
|
||||
]);
|
||||
|
||||
this.logger.succ('All charts successfully resynced.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class TickChartsProcessorService {
|
||||
@@ -45,7 +45,7 @@ export class TickChartsProcessorService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Tick charts...');
|
||||
|
||||
await Promise.all([
|
||||
@@ -64,6 +64,5 @@ export class TickChartsProcessorService {
|
||||
]);
|
||||
|
||||
this.logger.succ('All charts successfully ticked.');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { WebhooksRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -7,7 +8,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { WebhookDeliverJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -66,11 +66,11 @@ export class WebhookDeliverProcessorService {
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
|
@@ -585,7 +585,7 @@ export class ActivityPubServerService {
|
||||
name: request.params.emoji,
|
||||
});
|
||||
|
||||
if (emoji == null) {
|
||||
if (emoji == null || emoji.localOnly) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
@@ -201,7 +201,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
|
||||
fastify.register(this.clientServerService.createServer);
|
||||
|
||||
this.streamingApiServerService.attachStreamingApi(fastify.server);
|
||||
this.streamingApiServerService.attach(fastify.server);
|
||||
|
||||
fastify.server.on('error', err => {
|
||||
switch ((err as any).code) {
|
||||
@@ -229,7 +229,14 @@ export class ServerService implements OnApplicationShutdown {
|
||||
await fastify.ready();
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
await this.streamingApiServerService.detach();
|
||||
await this.#fastify.close();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -359,7 +359,12 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.userIpHistoriesClearIntervalId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
@@ -17,7 +17,7 @@ export class AuthenticationError extends Error {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticateService {
|
||||
export class AuthenticateService implements OnApplicationShutdown {
|
||||
private appCache: MemoryKVCache<App>;
|
||||
|
||||
constructor(
|
||||
@@ -36,7 +36,7 @@ export class AuthenticateService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> {
|
||||
if (token == null) {
|
||||
return [null, null];
|
||||
}
|
||||
@@ -85,4 +85,14 @@ export class AuthenticateService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.appCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
||||
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_reactions from './endpoints/users/reactions.js';
|
||||
@@ -330,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
|
||||
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
@@ -659,6 +661,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass:
|
||||
const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
|
||||
const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
|
||||
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
||||
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
||||
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
||||
const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
|
||||
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
||||
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
||||
const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
|
||||
@@ -668,7 +673,6 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas
|
||||
const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
|
||||
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
|
||||
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
|
||||
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
|
||||
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
|
||||
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
|
||||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
@@ -1001,6 +1005,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_push,
|
||||
$users_lists_show,
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_reactions,
|
||||
@@ -1010,7 +1017,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_searchByUsernameAndHost,
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
@@ -1335,6 +1341,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_push,
|
||||
$users_lists_show,
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_reactions,
|
||||
@@ -1344,7 +1353,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_searchByUsernameAndHost,
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
|
@@ -1,23 +1,27 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as websocket from 'websocket';
|
||||
import * as WebSocket from 'ws';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, AccessToken } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { AuthenticateService } from './AuthenticateService.js';
|
||||
import { LocalUser } from '@/models/entities/User';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
import type { ParsedUrlQuery } from 'querystring';
|
||||
import type * as http from 'node:http';
|
||||
|
||||
@Injectable()
|
||||
export class StreamingApiServerService {
|
||||
#wss: WebSocket.WebSocketServer;
|
||||
#connections = new Map<WebSocket.WebSocket, number>();
|
||||
#cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@@ -28,24 +32,6 @@ export class StreamingApiServerService {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private noteReadService: NoteReadService,
|
||||
private authenticateService: AuthenticateService,
|
||||
@@ -55,25 +41,65 @@ export class StreamingApiServerService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public attachStreamingApi(server: http.Server) {
|
||||
// Init websocket server
|
||||
const ws = new websocket.server({
|
||||
httpServer: server,
|
||||
public attach(server: http.Server): void {
|
||||
this.#wss = new WebSocket.WebSocketServer({
|
||||
noServer: true,
|
||||
});
|
||||
|
||||
ws.on('request', async (request) => {
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
|
||||
// TODO: トークンが間違ってるなどしてauthenticateに失敗したら
|
||||
// コネクション切断するなりエラーメッセージ返すなりする
|
||||
// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
|
||||
const [user, miapp] = await this.authenticateService.authenticate(q.i as string);
|
||||
|
||||
if (user?.isSuspended) {
|
||||
request.reject(400);
|
||||
server.on('upgrade', async (request, socket, head) => {
|
||||
if (request.url == null) {
|
||||
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const q = new URL(request.url, `http://${request.headers.host}`).searchParams;
|
||||
|
||||
let user: LocalUser | null = null;
|
||||
let app: AccessToken | null = null;
|
||||
|
||||
try {
|
||||
[user, app] = await this.authenticateService.authenticate(q.get('i'));
|
||||
} catch (e) {
|
||||
if (e instanceof AuthenticationError) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
} else {
|
||||
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||
}
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user?.isSuspended) {
|
||||
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
user, app,
|
||||
);
|
||||
|
||||
await stream.init();
|
||||
|
||||
this.#wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.#wss.emit('connection', ws, request, {
|
||||
stream, user, app,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
|
||||
stream: MainStreamConnection,
|
||||
user: LocalUser | null;
|
||||
app: AccessToken | null
|
||||
}) => {
|
||||
const { stream, user, app } = ctx;
|
||||
|
||||
const ev = new EventEmitter();
|
||||
|
||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||
@@ -83,21 +109,11 @@ export class StreamingApiServerService {
|
||||
|
||||
this.redisForSub.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
ev, user, miapp,
|
||||
);
|
||||
await stream.listen(ev, connection);
|
||||
|
||||
await main.init();
|
||||
this.#connections.set(connection, Date.now());
|
||||
|
||||
const connection = request.accept();
|
||||
|
||||
main.init2(connection);
|
||||
|
||||
const intervalId = user ? setInterval(() => {
|
||||
const userUpdateIntervalId = user ? setInterval(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
@@ -110,16 +126,39 @@ export class StreamingApiServerService {
|
||||
|
||||
connection.once('close', () => {
|
||||
ev.removeAllListeners();
|
||||
main.dispose();
|
||||
stream.dispose();
|
||||
this.redisForSub.off('message', onRedisMessage);
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
this.#connections.delete(connection);
|
||||
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
|
||||
});
|
||||
|
||||
connection.on('message', async (data) => {
|
||||
if (data.type === 'utf8' && data.utf8Data === 'ping') {
|
||||
connection.send('pong');
|
||||
}
|
||||
connection.on('pong', () => {
|
||||
this.#connections.set(connection, Date.now());
|
||||
});
|
||||
});
|
||||
|
||||
// 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする
|
||||
this.#cleanConnectionsIntervalId = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [connection, lastActive] of this.#connections.entries()) {
|
||||
if (now - lastActive > 1000 * 60 * 2) {
|
||||
connection.terminate();
|
||||
this.#connections.delete(connection);
|
||||
} else {
|
||||
connection.ping();
|
||||
}
|
||||
}
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public detach(): Promise<void> {
|
||||
if (this.#cleanConnectionsIntervalId) {
|
||||
clearInterval(this.#cleanConnectionsIntervalId);
|
||||
this.#cleanConnectionsIntervalId = null;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.#wss.close(() => resolve());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js';
|
||||
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
||||
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
@@ -330,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
|
||||
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
@@ -656,7 +658,10 @@ const eps = [
|
||||
['users/lists/pull', ep___users_lists_pull],
|
||||
['users/lists/push', ep___users_lists_push],
|
||||
['users/lists/show', ep___users_lists_show],
|
||||
['users/lists/favorite', ep___users_lists_favorite],
|
||||
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
||||
['users/lists/update', ep___users_lists_update],
|
||||
['users/lists/create-from-public', ep___users_lists_create_from_public],
|
||||
['users/notes', ep___users_notes],
|
||||
['users/pages', ep___users_pages],
|
||||
['users/reactions', ep___users_reactions],
|
||||
@@ -666,7 +671,6 @@ const eps = [
|
||||
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
|
||||
['users/search', ep___users_search],
|
||||
['users/show', ep___users_show],
|
||||
['users/stats', ep___users_stats],
|
||||
['users/achievements', ep___users_achievements],
|
||||
['users/update-memo', ep___users_updateMemo],
|
||||
['fetch-rss', ep___fetchRss],
|
||||
|
@@ -25,7 +25,7 @@ export const paramDef = {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
title: { type: 'string', minLength: 1 },
|
||||
text: { type: 'string', minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 0 },
|
||||
},
|
||||
required: ['id', 'title', 'text', 'imageUrl'],
|
||||
} as const;
|
||||
@@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
|
||||
imageUrl: ps.imageUrl || null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -25,9 +25,24 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['fileId'],
|
||||
required: ['name', 'fileId'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
@@ -45,18 +60,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
||||
const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||
|
||||
const emoji = await this.customEmojiService.add({
|
||||
driveFile,
|
||||
name,
|
||||
category: null,
|
||||
aliases: [],
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
host: null,
|
||||
license: null,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -15,6 +17,11 @@ export const meta = {
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||
},
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
|
||||
},
|
||||
sameNameEmojiExists: {
|
||||
message: 'Emoji that have same name already exists.',
|
||||
code: 'SAME_NAME_EMOJI_EXISTS',
|
||||
@@ -28,6 +35,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
@@ -37,6 +45,11 @@ export const paramDef = {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['id', 'name', 'aliases'],
|
||||
} as const;
|
||||
@@ -45,14 +58,28 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let driveFile;
|
||||
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive,
|
||||
localOnly: ps.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -61,10 +61,17 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
errorImageUrl: {
|
||||
serverErrorImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
infoImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
notFoundImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
default: 'https://xn--931a.moe/aiart/yubitun.png',
|
||||
},
|
||||
iconUrl: {
|
||||
type: 'string',
|
||||
@@ -305,7 +312,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl,
|
||||
bannerUrl: instance.bannerUrl,
|
||||
errorImageUrl: instance.errorImageUrl,
|
||||
serverErrorImageUrl: instance.serverErrorImageUrl,
|
||||
notFoundImageUrl: instance.notFoundImageUrl,
|
||||
infoImageUrl: instance.infoImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
|
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
||||
} catch {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
|
@@ -32,7 +32,9 @@ export const paramDef = {
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
mascotImageUrl: { type: 'string', nullable: true },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
errorImageUrl: { type: 'string', nullable: true },
|
||||
serverErrorImageUrl: { type: 'string', nullable: true },
|
||||
infoImageUrl: { type: 'string', nullable: true },
|
||||
notFoundImageUrl: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
backgroundImageUrl: { type: 'string', nullable: true },
|
||||
logoImageUrl: { type: 'string', nullable: true },
|
||||
@@ -149,6 +151,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.iconUrl = ps.iconUrl;
|
||||
}
|
||||
|
||||
if (ps.serverErrorImageUrl !== undefined) {
|
||||
set.serverErrorImageUrl = ps.serverErrorImageUrl;
|
||||
}
|
||||
|
||||
if (ps.infoImageUrl !== undefined) {
|
||||
set.infoImageUrl = ps.infoImageUrl;
|
||||
}
|
||||
|
||||
if (ps.notFoundImageUrl !== undefined) {
|
||||
set.notFoundImageUrl = ps.notFoundImageUrl;
|
||||
}
|
||||
|
||||
if (ps.backgroundImageUrl !== undefined) {
|
||||
set.backgroundImageUrl = ps.backgroundImageUrl;
|
||||
}
|
||||
@@ -281,10 +295,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.smtpPass = ps.smtpPass;
|
||||
}
|
||||
|
||||
if (ps.errorImageUrl !== undefined) {
|
||||
set.errorImageUrl = ps.errorImageUrl;
|
||||
}
|
||||
|
||||
if (ps.enableServiceWorker !== undefined) {
|
||||
set.enableServiceWorker = ps.enableServiceWorker;
|
||||
}
|
||||
|
@@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
|
@@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
const accessToken = secureRndstr(32, true);
|
||||
|
||||
// Fetch exist access token
|
||||
@@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
// Lookup app
|
||||
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
||||
|
||||
// Generate Hash
|
||||
@@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Insert access token doc
|
||||
await this.accessTokensRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: now,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { promisify } from 'node:util';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as cbor from 'cbor';
|
||||
import cbor from 'cbor';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
@@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.accessTokensRepository.createQueryBuilder('token')
|
||||
.where('token.userId = :userId', { userId: me.id });
|
||||
.where('token.userId = :userId', { userId: me.id })
|
||||
.leftJoinAndSelect('token.app', 'app');
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
|
||||
@@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
return await Promise.all(tokens.map(token => ({
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
name: token.name ?? token.app?.name,
|
||||
createdAt: token.createdAt,
|
||||
lastUsedAt: token.lastUsedAt,
|
||||
permission: token.permission,
|
||||
|
@@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||
'-',
|
||||
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
|
||||
'COUNT', limit);
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
|
@@ -141,13 +141,12 @@ export const paramDef = {
|
||||
preventAiLearning: { type: 'boolean' },
|
||||
isBot: { type: 'boolean' },
|
||||
isCat: { type: 'boolean' },
|
||||
showTimelineReplies: { type: 'boolean' },
|
||||
injectFeaturedNote: { type: 'boolean' },
|
||||
receiveAnnouncementEmail: { type: 'boolean' },
|
||||
alwaysMarkNsfw: { type: 'boolean' },
|
||||
autoSensitive: { type: 'boolean' },
|
||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id' },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: { type: 'array' },
|
||||
mutedInstances: { type: 'array', items: {
|
||||
type: 'string',
|
||||
@@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
||||
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
|
||||
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||
if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
|
||||
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user