Merge remote-tracking branch 'misskey/release/2024.5.0' into future
This commit is contained in:
commit
3372e0ffe1
181 changed files with 4057 additions and 891 deletions
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ChannelIdDenormalizedForMiPoll1716129964060 {
|
||||
name = 'ChannelIdDenormalizedForMiPoll1716129964060'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `);
|
||||
await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class NotRespondingSince1716345015347 {
|
||||
name = 'NotRespondingSince1716345015347'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SuspensionStateInsteadOfIsSspended1716345771510 {
|
||||
name = 'SuspensionStateInsteadOfIsSspended1716345771510'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
|
||||
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
|
||||
CASE "suspensionState"
|
||||
WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
|
||||
ELSE 'none'::instance_suspensionstate_enum
|
||||
END
|
||||
)`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
|
||||
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
|
||||
CASE "suspensionState"
|
||||
WHEN 'none'::instance_suspensionstate_enum THEN FALSE
|
||||
ELSE TRUE
|
||||
END
|
||||
)`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
|
||||
|
||||
await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
|
||||
|
||||
await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemoveAntennaNotify1716450883149 {
|
||||
name = 'RemoveAntennaNotify1716450883149'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`);
|
||||
}
|
||||
}
|
16
packages/backend/migration/1717117195275-inquiryUrl.js
Normal file
16
packages/backend/migration/1717117195275-inquiryUrl.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class InquiryUrl1717117195275 {
|
||||
name = 'InquiryUrl1717117195275'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.10.0"
|
||||
"node": "^20.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
|
@ -84,6 +84,8 @@
|
|||
"@nestjs/core": "10.3.8",
|
||||
"@nestjs/testing": "10.3.8",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "^8.5.0",
|
||||
"@sentry/profiling-node": "^8.5.0",
|
||||
"@simplewebauthn/server": "10.0.0",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
|
@ -119,7 +121,7 @@
|
|||
"form-data": "4.0.0",
|
||||
"glob": "10.3.10",
|
||||
"got": "14.2.1",
|
||||
"happy-dom": "14.7.1",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
|
|
|
@ -15,6 +15,7 @@ import Logger from '@/logger.js';
|
|||
import { envOption } from '../env.js';
|
||||
import { masterMain } from './master.js';
|
||||
import { workerMain } from './worker.js';
|
||||
import { readyRef } from './ready.js';
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
|
@ -79,6 +80,8 @@ async function main() {
|
|||
await workerMain();
|
||||
}
|
||||
|
||||
readyRef.value = true;
|
||||
|
||||
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
||||
// それ以外のときは process.send は使えないので弾く
|
||||
if (process.send) {
|
||||
|
|
|
@ -10,6 +10,8 @@ import * as os from 'node:os';
|
|||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import Logger from '@/logger.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -74,6 +76,24 @@ export async function masterMain() {
|
|||
|
||||
bootLogger.succ('Sharkey initialized');
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
|
||||
if (envOption.disableClustering) {
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
|
|
6
packages/backend/src/boot/ready.ts
Normal file
6
packages/backend/src/boot/ready.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const readyRef = { value: false };
|
|
@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { globSync } from 'glob';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
|
@ -57,6 +58,8 @@ type Source = {
|
|||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
|
||||
|
||||
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
|
||||
|
||||
|
@ -174,6 +177,8 @@ export type Config = {
|
|||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||
perChannelMaxNoteCacheCount: number;
|
||||
perUserNotificationsMaxCount: number;
|
||||
deactivateAntennaThreshold: number;
|
||||
|
@ -251,6 +256,8 @@ export function loadConfig(): Config {
|
|||
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||
sentryForBackend: config.sentryForBackend,
|
||||
sentryForFrontend: config.sentryForFrontend,
|
||||
id: config.id,
|
||||
proxy: config.proxy,
|
||||
proxySmtp: config.proxySmtp,
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Brackets, EntityNotFoundError } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
|
@ -29,6 +30,7 @@ export class AnnouncementService {
|
|||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -79,7 +81,7 @@ export class AnnouncementService {
|
|||
userId: values.userId,
|
||||
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const packed = (await this.packMany([announcement]))[0];
|
||||
const packed = await this.announcementEntityService.pack(announcement);
|
||||
|
||||
if (values.userId) {
|
||||
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
||||
|
@ -177,6 +179,24 @@ export class AnnouncementService {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
|
||||
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
|
||||
if (me) {
|
||||
if (announcement.userId && announcement.userId !== me.id) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: announcement.id,
|
||||
userId: me.id,
|
||||
});
|
||||
return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
|
||||
} else {
|
||||
return this.announcementEntityService.pack(announcement, null);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
|
||||
try {
|
||||
|
@ -193,29 +213,4 @@ export class AnnouncementService {
|
|||
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
announcements: MiAnnouncement[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
options?: {
|
||||
reads?: MiAnnouncementRead[];
|
||||
},
|
||||
): Promise<Packed<'Announcement'>[]> {
|
||||
const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
|
||||
return announcements.map(announcement => ({
|
||||
id: announcement.id,
|
||||
createdAt: this.idService.parse(announcement.id).date.toISOString(),
|
||||
updatedAt: announcement.updatedAt?.toISOString() ?? null,
|
||||
text: announcement.text,
|
||||
title: announcement.title,
|
||||
imageUrl: announcement.imageUrl,
|
||||
icon: announcement.icon,
|
||||
display: announcement.display,
|
||||
needConfirmationToRead: announcement.needConfirmationToRead,
|
||||
silence: announcement.silence,
|
||||
forYou: announcement.userId === me?.id,
|
||||
isRead: reads.some(read => read.announcementId === announcement.id),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@ import ApRequestChart from './chart/charts/ap-request.js';
|
|||
import { ChartManagementService } from './chart/ChartManagementService.js';
|
||||
|
||||
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
|
||||
import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js';
|
||||
import { AntennaEntityService } from './entities/AntennaEntityService.js';
|
||||
import { AppEntityService } from './entities/AppEntityService.js';
|
||||
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
|
||||
|
@ -223,6 +224,7 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe
|
|||
const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService };
|
||||
|
||||
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
|
||||
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
|
||||
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
|
||||
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
|
||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
||||
|
@ -363,6 +365,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
|
@ -499,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
|
@ -635,6 +639,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
|
@ -770,6 +775,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
|
|
|
@ -369,10 +369,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
|
||||
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
|
||||
const res = {} as any;
|
||||
const res = {} as Record<string, string>;
|
||||
for (let i = 0; i < emojiNames.length; i++) {
|
||||
if (emojis[i] != null) {
|
||||
res[emojiNames[i]] = emojis[i];
|
||||
const resolvedEmoji = emojis[i];
|
||||
if (resolvedEmoji != null) {
|
||||
res[emojiNames[i]] = resolvedEmoji;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
|
|
|
@ -477,14 +477,20 @@ export class DriveService {
|
|||
|
||||
if (user && !force) {
|
||||
// Check if there is a file with the same hash
|
||||
const much = await this.driveFilesRepository.findOneBy({
|
||||
const matched = await this.driveFilesRepository.findOneBy({
|
||||
md5: info.md5,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (much) {
|
||||
this.registerLogger.info(`file with same hash is found: ${much.id}`);
|
||||
return much;
|
||||
if (matched) {
|
||||
this.registerLogger.info(`file with same hash is found: ${matched.id}`);
|
||||
if (sensitive && !matched.isSensitive) {
|
||||
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
||||
// Therefore, update the file to sensitive.
|
||||
await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
|
||||
matched.isSensitive = true;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,8 +62,8 @@ export class FanoutTimelineEndpointService {
|
|||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||
|
||||
const shouldPrepend = ps.sinceId && !ps.untilId;
|
||||
const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
|
||||
const ascending = ps.sinceId && !ps.untilId;
|
||||
const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
|
||||
|
||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||
|
||||
|
@ -148,9 +148,7 @@ export class FanoutTimelineEndpointService {
|
|||
|
||||
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
|
||||
// 十分Redisからとれた
|
||||
const result = redisTimeline.slice(0, ps.limit);
|
||||
if (shouldPrepend) result.reverse();
|
||||
return result;
|
||||
return redisTimeline.slice(0, ps.limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,8 +156,7 @@ export class FanoutTimelineEndpointService {
|
|||
const remainingToRead = ps.limit - redisTimeline.length;
|
||||
let dbUntil: string | null;
|
||||
let dbSince: string | null;
|
||||
if (shouldPrepend) {
|
||||
redisTimeline.reverse();
|
||||
if (ascending) {
|
||||
dbUntil = ps.untilId;
|
||||
dbSince = noteIds[noteIds.length - 1];
|
||||
} else {
|
||||
|
@ -167,7 +164,7 @@ export class FanoutTimelineEndpointService {
|
|||
dbSince = ps.sinceId;
|
||||
}
|
||||
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
|
||||
return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb];
|
||||
return [...redisTimeline, ...gotFromDb];
|
||||
}
|
||||
|
||||
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
||||
|
|
|
@ -154,7 +154,7 @@ export class FetchInstanceMetadataService {
|
|||
throw new Error('No wellknown links');
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
const links = wellknown.links as ({ rel: string, href: string; })[];
|
||||
|
||||
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
|
|
|
@ -662,6 +662,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
noteVisibility: insert.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
channelId: insert.channelId,
|
||||
});
|
||||
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
|
|
|
@ -28,6 +28,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserR
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
|
@ -36,9 +37,8 @@ import { ApResolverService } from './ApResolverService.js';
|
|||
import { ApAudienceService } from './ApAudienceService.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Resolver } from './ApResolverService.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApInboxService {
|
||||
|
@ -90,13 +90,15 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
let result = undefined as string | void;
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
const results = [] as [string, string | void][];
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
const act = await resolver.resolve(item);
|
||||
try {
|
||||
await this.performOneActivity(actor, act);
|
||||
results.push([getApId(item), await this.performOneActivity(actor, act)]);
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
this.logger.error(err);
|
||||
|
@ -105,8 +107,13 @@ export class ApInboxService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
|
||||
if (hasReason) {
|
||||
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
|
||||
}
|
||||
} else {
|
||||
await this.performOneActivity(actor, activity);
|
||||
result = await this.performOneActivity(actor, activity);
|
||||
}
|
||||
|
||||
// ついでにリモートユーザーの情報が古かったら更新しておく
|
||||
|
@ -117,42 +124,43 @@ export class ApInboxService {
|
|||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (isCreate(activity)) {
|
||||
await this.create(actor, activity);
|
||||
return await this.create(actor, activity);
|
||||
} else if (isDelete(activity)) {
|
||||
await this.delete(actor, activity);
|
||||
return await this.delete(actor, activity);
|
||||
} else if (isUpdate(activity)) {
|
||||
await this.update(actor, activity);
|
||||
return await this.update(actor, activity);
|
||||
} else if (isFollow(activity)) {
|
||||
await this.follow(actor, activity);
|
||||
return await this.follow(actor, activity);
|
||||
} else if (isAccept(activity)) {
|
||||
await this.accept(actor, activity);
|
||||
return await this.accept(actor, activity);
|
||||
} else if (isReject(activity)) {
|
||||
await this.reject(actor, activity);
|
||||
return await this.reject(actor, activity);
|
||||
} else if (isAdd(activity)) {
|
||||
await this.add(actor, activity).catch(err => this.logger.error(err));
|
||||
return await this.add(actor, activity);
|
||||
} else if (isRemove(activity)) {
|
||||
await this.remove(actor, activity).catch(err => this.logger.error(err));
|
||||
return await this.remove(actor, activity);
|
||||
} else if (isAnnounce(activity)) {
|
||||
await this.announce(actor, activity);
|
||||
return await this.announce(actor, activity);
|
||||
} else if (isLike(activity)) {
|
||||
await this.like(actor, activity);
|
||||
return await this.like(actor, activity);
|
||||
} else if (isUndo(activity)) {
|
||||
await this.undo(actor, activity);
|
||||
return await this.undo(actor, activity);
|
||||
} else if (isBlock(activity)) {
|
||||
await this.block(actor, activity);
|
||||
return await this.block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
await this.flag(actor, activity);
|
||||
return await this.flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await this.move(actor, activity);
|
||||
return await this.move(actor, activity);
|
||||
} else {
|
||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||
return `unrecognized activity type: ${activity.type}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,38 +242,49 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> {
|
||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
return 'target is null';
|
||||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.addPinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
return `unknown target: ${activity.target}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> {
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Announce: ${uri}`);
|
||||
|
||||
const targetUri = getApId(activity.object);
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
await this.announceNote(actor, activity, targetUri);
|
||||
if (!activity.object) return 'skip: activity has no object property';
|
||||
const targetUri = getApId(activity.object);
|
||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
||||
|
||||
const target = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
return e;
|
||||
});
|
||||
|
||||
if (isPost(target)) return await this.announceNote(actor, activity, target);
|
||||
|
||||
return `skip: unknown object type ${getApType(target)}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
if (actor.isSuspended) {
|
||||
|
@ -288,24 +307,21 @@ export class ApInboxService {
|
|||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await this.apNoteService.resolveNote(targetUri);
|
||||
if (renote == null) throw new Error('announce target is null');
|
||||
renote = await this.apNoteService.resolveNote(target);
|
||||
if (renote == null) return 'announce target is null';
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (!err.isRetryable) {
|
||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||
return;
|
||||
return `Ignored announce target ${target.id} - ${err.statusCode}`;
|
||||
}
|
||||
|
||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
|
||||
return `Error in announce target ${target.id} - ${err.statusCode}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
|
||||
this.logger.warn('skip: invalid actor for this activity');
|
||||
return;
|
||||
return 'skip: invalid actor for this activity';
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
@ -314,8 +330,7 @@ export class ApInboxService {
|
|||
const createdAt = activity.published ? new Date(activity.published) : null;
|
||||
|
||||
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
||||
this.logger.warn('skip: malformed createdAt');
|
||||
return;
|
||||
return 'skip: malformed createdAt';
|
||||
}
|
||||
|
||||
await this.noteCreateService.create(actor, {
|
||||
|
@ -349,11 +364,15 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
|
||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Create: ${uri}`);
|
||||
|
||||
if (!activity.object) return 'skip: activity has no object property';
|
||||
const targetUri = getApId(activity.object);
|
||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
||||
|
||||
// copy audiences between activity <=> object.
|
||||
if (typeof activity.object === 'object') {
|
||||
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
|
||||
|
@ -380,7 +399,7 @@ export class ApInboxService {
|
|||
if (isPost(object)) {
|
||||
await this.createNote(resolver, actor, object, false, activity);
|
||||
} else {
|
||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||
return `Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -422,7 +441,7 @@ export class ApInboxService {
|
|||
@bindThis
|
||||
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
// 削除対象objectのtype
|
||||
|
@ -581,29 +600,29 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> {
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
return 'target is null';
|
||||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.removePinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
return `unknown target: ${activity.target}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
const uri = activity.id ?? activity;
|
||||
|
@ -614,7 +633,7 @@ export class ApInboxService {
|
|||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
return e;
|
||||
});
|
||||
|
||||
// don't queue because the sender may attempt again when timeout
|
||||
|
|
|
@ -86,20 +86,20 @@ export class ApNoteService {
|
|||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
|
||||
}
|
||||
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
}
|
||||
|
||||
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
|
||||
if (object.attributedTo && actualHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
}
|
||||
|
||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
||||
return new Error('invalid Note: published timestamp is malformed');
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -334,3 +334,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
|
|||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
|
||||
|
|
|
@ -10,6 +10,8 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
|||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -26,6 +28,11 @@ export class AbuseUserReportEntityService {
|
|||
@bindThis
|
||||
public async pack(
|
||||
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
|
||||
hint?: {
|
||||
packedReporter?: Packed<'UserDetailedNotMe'>,
|
||||
packedTargetUser?: Packed<'UserDetailedNotMe'>,
|
||||
packedAssignee?: Packed<'UserDetailedNotMe'>,
|
||||
},
|
||||
) {
|
||||
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -37,13 +44,13 @@ export class AbuseUserReportEntityService {
|
|||
reporterId: report.reporterId,
|
||||
targetUserId: report.targetUserId,
|
||||
assigneeId: report.assigneeId,
|
||||
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
|
||||
reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
|
||||
targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
|
||||
assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
|
@ -51,9 +58,24 @@ export class AbuseUserReportEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
reports: any[],
|
||||
public async packMany(
|
||||
reports: MiAbuseUserReport[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
|
||||
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
|
||||
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
|
||||
const _userMap = await this.userEntityService.packMany(
|
||||
[..._reporters, ..._targetUsers, ..._assignees],
|
||||
null,
|
||||
{ schema: 'UserDetailedNotMe' },
|
||||
).then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
reports.map(report => {
|
||||
const packedReporter = _userMap.get(report.reporterId);
|
||||
const packedTargetUser = _userMap.get(report.targetUserId);
|
||||
const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
|
||||
return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AnnouncementEntityService {
|
||||
constructor(
|
||||
@Inject(DI.announcementsRepository)
|
||||
private announcementsRepository: AnnouncementsRepository,
|
||||
|
||||
@Inject(DI.announcementReadsRepository)
|
||||
private announcementReadsRepository: AnnouncementReadsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null },
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'Announcement'>> {
|
||||
const announcement = typeof src === 'object'
|
||||
? src
|
||||
: await this.announcementsRepository.findOneByOrFail({
|
||||
id: src,
|
||||
}) as MiAnnouncement & { isRead?: boolean | null };
|
||||
|
||||
if (me && announcement.isRead === undefined) {
|
||||
announcement.isRead = await this.announcementReadsRepository
|
||||
.countBy({
|
||||
announcementId: announcement.id,
|
||||
userId: me.id,
|
||||
})
|
||||
.then((count: number) => count > 0);
|
||||
}
|
||||
|
||||
return {
|
||||
id: announcement.id,
|
||||
createdAt: this.idService.parse(announcement.id).date.toISOString(),
|
||||
updatedAt: announcement.updatedAt?.toISOString() ?? null,
|
||||
title: announcement.title,
|
||||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
icon: announcement.icon,
|
||||
display: announcement.display,
|
||||
forYou: announcement.userId === me?.id,
|
||||
needConfirmationToRead: announcement.needConfirmationToRead,
|
||||
silence: announcement.silence,
|
||||
isRead: announcement.isRead !== null ? announcement.isRead : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) : Promise<Packed<'Announcement'>[]> {
|
||||
return (await Promise.allSettled(announcements.map(x => this.pack(x, me))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<'Announcement'>>).value);
|
||||
}
|
||||
}
|
|
@ -38,12 +38,12 @@ export class AntennaEntityService {
|
|||
users: antenna.users,
|
||||
caseSensitive: antenna.caseSensitive,
|
||||
localOnly: antenna.localOnly,
|
||||
notify: antenna.notify,
|
||||
excludeBots: antenna.excludeBots,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
isActive: antenna.isActive,
|
||||
hasUnreadNote: false, // TODO
|
||||
notify: false, // 後方互換性のため
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ export class BlockingEntityService {
|
|||
public async pack(
|
||||
src: MiBlocking['id'] | MiBlocking,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
blockee?: Packed<'UserDetailedNotMe'>,
|
||||
},
|
||||
): Promise<Packed<'Blocking'>> {
|
||||
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -36,17 +39,20 @@ export class BlockingEntityService {
|
|||
id: blocking.id,
|
||||
createdAt: this.idService.parse(blocking.id).date.toISOString(),
|
||||
blockeeId: blocking.blockeeId,
|
||||
blockee: this.userEntityService.pack(blocking.blockeeId, me, {
|
||||
blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
blockings: any[],
|
||||
public async packMany(
|
||||
blockings: MiBlocking[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
return Promise.all(blockings.map(x => this.pack(x, me)));
|
||||
const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
|
||||
const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ export class ClipEntityService {
|
|||
public async pack(
|
||||
src: MiClip['id'] | MiClip,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'Clip'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
|
||||
|
@ -44,7 +47,7 @@ export class ClipEntityService {
|
|||
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
||||
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
|
||||
userId: clip.userId,
|
||||
user: this.userEntityService.pack(clip.user ?? clip.userId),
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
|
||||
name: clip.name,
|
||||
description: clip.description,
|
||||
isPublic: clip.isPublic,
|
||||
|
@ -55,11 +58,14 @@ export class ClipEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
public async packMany(
|
||||
clips: MiClip[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(clips.map(x => this.pack(x, me)));
|
||||
const _users = clips.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -222,6 +222,9 @@ export class DriveFileEntityService {
|
|||
public async packNullable(
|
||||
src: MiDriveFile['id'] | MiDriveFile,
|
||||
options?: PackOptions,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'DriveFile'> | null> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
|
@ -249,7 +252,7 @@ export class DriveFileEntityService {
|
|||
detail: true,
|
||||
}) : null,
|
||||
userId: file.userId,
|
||||
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
|
||||
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -258,7 +261,10 @@ export class DriveFileEntityService {
|
|||
files: MiDriveFile[],
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
|
||||
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
|
||||
const _userMap = await this.userEntityService.packMany(_user)
|
||||
.then(users => new Map(users.map(user => [user.id, user])));
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
|
||||
return items.filter(isNotNull);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,9 @@ export class FlashEntityService {
|
|||
public async pack(
|
||||
src: MiFlash['id'] | MiFlash,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'Flash'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
|
||||
|
@ -42,7 +45,7 @@ export class FlashEntityService {
|
|||
createdAt: this.idService.parse(flash.id).date.toISOString(),
|
||||
updatedAt: flash.updatedAt.toISOString(),
|
||||
userId: flash.userId,
|
||||
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
|
@ -52,11 +55,14 @@ export class FlashEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
flashs: MiFlash[],
|
||||
public async packMany(
|
||||
flashes: MiFlash[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(flashs.map(x => this.pack(x, me)));
|
||||
const _users = flashes.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -26,14 +27,36 @@ export class FollowRequestEntityService {
|
|||
public async pack(
|
||||
src: MiFollowRequest['id'] | MiFollowRequest,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedFollower?: Packed<'UserLite'>,
|
||||
packedFollowee?: Packed<'UserLite'>,
|
||||
},
|
||||
) {
|
||||
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: request.id,
|
||||
follower: await this.userEntityService.pack(request.followerId, me),
|
||||
followee: await this.userEntityService.pack(request.followeeId, me),
|
||||
follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
|
||||
followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
requests: MiFollowRequest[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
|
||||
const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
|
||||
const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
requests.map(req => {
|
||||
const packedFollower = _userMap.get(req.followerId);
|
||||
const packedFollowee = _userMap.get(req.followeeId);
|
||||
return this.pack(req, me, { packedFollower, packedFollowee });
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,10 @@ export class FollowingEntityService {
|
|||
populateFollowee?: boolean;
|
||||
populateFollower?: boolean;
|
||||
},
|
||||
hint?: {
|
||||
packedFollowee?: Packed<'UserDetailedNotMe'>,
|
||||
packedFollower?: Packed<'UserDetailedNotMe'>,
|
||||
},
|
||||
): Promise<Packed<'Following'>> {
|
||||
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -88,25 +92,35 @@ export class FollowingEntityService {
|
|||
createdAt: this.idService.parse(following.id).date.toISOString(),
|
||||
followeeId: following.followeeId,
|
||||
followerId: following.followerId,
|
||||
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
|
||||
followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}) : undefined,
|
||||
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
|
||||
follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
followings: any[],
|
||||
public async packMany(
|
||||
followings: MiFollowing[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
opts?: {
|
||||
populateFollowee?: boolean;
|
||||
populateFollower?: boolean;
|
||||
},
|
||||
) {
|
||||
return Promise.all(followings.map(x => this.pack(x, me, opts)));
|
||||
const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
|
||||
const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
|
||||
const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
followings.map(following => {
|
||||
const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
|
||||
const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
|
||||
return this.pack(following, me, opts, { packedFollowee, packedFollower });
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,9 @@ export class GalleryPostEntityService {
|
|||
public async pack(
|
||||
src: MiGalleryPost['id'] | MiGalleryPost,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'GalleryPost'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
|
||||
|
@ -44,7 +47,7 @@ export class GalleryPostEntityService {
|
|||
createdAt: this.idService.parse(post.id).date.toISOString(),
|
||||
updatedAt: post.updatedAt.toISOString(),
|
||||
userId: post.userId,
|
||||
user: this.userEntityService.pack(post.user ?? post.userId, me),
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
fileIds: post.fileIds,
|
||||
|
@ -58,11 +61,14 @@ export class GalleryPostEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
public async packMany(
|
||||
posts: MiGalleryPost[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(posts.map(x => this.pack(x, me)));
|
||||
const _users = posts.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,8 @@ export class InstanceEntityService {
|
|||
followingCount: instance.followingCount,
|
||||
followersCount: instance.followersCount,
|
||||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.isSuspended,
|
||||
isSuspended: instance.suspensionState !== 'none',
|
||||
suspensionState: instance.suspensionState,
|
||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { MiUser } from '@/models/User.js';
|
|||
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -29,6 +30,10 @@ export class InviteCodeEntityService {
|
|||
public async pack(
|
||||
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hints?: {
|
||||
packedCreatedBy?: Packed<'UserLite'>,
|
||||
packedUsedBy?: Packed<'UserLite'>,
|
||||
},
|
||||
): Promise<Packed<'InviteCode'>> {
|
||||
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
|
||||
where: {
|
||||
|
@ -42,18 +47,28 @@ export class InviteCodeEntityService {
|
|||
code: target.code,
|
||||
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
|
||||
createdAt: this.idService.parse(target.id).date.toISOString(),
|
||||
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
|
||||
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
|
||||
createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
|
||||
usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
|
||||
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
|
||||
used: !!target.usedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
targets: any[],
|
||||
public async packMany(
|
||||
tickets: MiRegistrationTicket[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
return Promise.all(targets.map(x => this.pack(x, me)));
|
||||
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
|
||||
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
|
||||
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
tickets.map(ticket => {
|
||||
const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
|
||||
const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
|
||||
return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ export class MetaEntityService {
|
|||
impressumUrl: instance.impressumUrl,
|
||||
donationUrl: instance.donationUrl,
|
||||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
|
|
|
@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { ModerationLogsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -27,6 +28,9 @@ export class ModerationLogEntityService {
|
|||
@bindThis
|
||||
public async pack(
|
||||
src: MiModerationLog['id'] | MiModerationLog,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserDetailedNotMe'>,
|
||||
},
|
||||
) {
|
||||
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -36,17 +40,20 @@ export class ModerationLogEntityService {
|
|||
type: log.type,
|
||||
info: log.info,
|
||||
userId: log.userId,
|
||||
user: this.userEntityService.pack(log.user ?? log.userId, null, {
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
reports: any[],
|
||||
public async packMany(
|
||||
reports: MiModerationLog[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
const _users = reports.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ export class MutingEntityService {
|
|||
public async pack(
|
||||
src: MiMuting['id'] | MiMuting,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hints?: {
|
||||
packedMutee?: Packed<'UserDetailedNotMe'>,
|
||||
},
|
||||
): Promise<Packed<'Muting'>> {
|
||||
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -38,18 +41,21 @@ export class MutingEntityService {
|
|||
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
||||
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
|
||||
muteeId: muting.muteeId,
|
||||
mutee: this.userEntityService.pack(muting.muteeId, me, {
|
||||
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
mutings: any[],
|
||||
public async packMany(
|
||||
mutings: MiMuting[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
return Promise.all(mutings.map(x => this.pack(x, me)));
|
||||
const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
|
||||
const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -307,6 +307,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
_hint_?: {
|
||||
myReactions: Map<MiNote['id'], string | null>;
|
||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'Note'>> {
|
||||
|
@ -336,13 +337,14 @@ export class NoteEntityService implements OnModuleInit {
|
|||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
const packedUsers = options?._hint_?.packedUsers;
|
||||
|
||||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
||||
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||
userId: note.userId,
|
||||
user: this.userEntityService.pack(note.user ?? note.userId, me),
|
||||
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
|
||||
text: text,
|
||||
cw: note.cw,
|
||||
visibility: note.visibility,
|
||||
|
@ -465,12 +467,20 @@ export class NoteEntityService implements OnModuleInit {
|
|||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
||||
const users = [
|
||||
...notes.map(({ user, userId }) => user ?? userId),
|
||||
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
|
||||
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
|
||||
];
|
||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
myReactions: myReactionsMap,
|
||||
packedFiles,
|
||||
packedUsers,
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
options?: {
|
||||
withNote: boolean;
|
||||
},
|
||||
hints?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'NoteReaction'>> {
|
||||
const opts = Object.assign({
|
||||
withNote: false,
|
||||
|
@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
return {
|
||||
id: reaction.id,
|
||||
createdAt: this.idService.parse(reaction.id).date.toISOString(),
|
||||
user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
|
||||
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
|
||||
type: this.reactionService.convertLegacyReaction(reaction.reaction),
|
||||
...(opts.withNote ? {
|
||||
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
|
||||
|
@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
const opts = Object.assign({
|
||||
withNote: false,
|
||||
}, options);
|
||||
|
||||
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
|
||||
const _users = reactions.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ export class PageEntityService {
|
|||
public async pack(
|
||||
src: MiPage['id'] | MiPage,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'Page'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
|
||||
|
@ -91,7 +94,7 @@ export class PageEntityService {
|
|||
createdAt: this.idService.parse(page.id).date.toISOString(),
|
||||
updatedAt: page.updatedAt.toISOString(),
|
||||
userId: page.userId,
|
||||
user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
content: page.content,
|
||||
variables: page.variables,
|
||||
title: page.title,
|
||||
|
@ -110,11 +113,14 @@ export class PageEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
public async packMany(
|
||||
pages: MiPage[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(pages.map(x => this.pack(x, me)));
|
||||
const _users = pages.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
|
|||
public async pack(
|
||||
src: MiRenoteMuting['id'] | MiRenoteMuting,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hints?: {
|
||||
packedMutee?: Packed<'UserDetailedNotMe'>
|
||||
},
|
||||
): Promise<Packed<'RenoteMuting'>> {
|
||||
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
|
@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
|
|||
id: muting.id,
|
||||
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
||||
muteeId: muting.muteeId,
|
||||
mutee: this.userEntityService.pack(muting.muteeId, me, {
|
||||
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
mutings: any[],
|
||||
public async packMany(
|
||||
mutings: MiRenoteMuting[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
return Promise.all(mutings.map(x => this.pack(x, me)));
|
||||
const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,13 +28,15 @@ export class ReversiGameEntityService {
|
|||
@bindThis
|
||||
public async packDetail(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
hint?: {
|
||||
packedUser1?: Packed<'UserLite'>,
|
||||
packedUser2?: Packed<'UserLite'>,
|
||||
},
|
||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await Promise.all([
|
||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||
]);
|
||||
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
|
||||
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
|
@ -49,10 +51,10 @@ export class ReversiGameEntityService {
|
|||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
user1,
|
||||
user2,
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
|
@ -68,22 +70,35 @@ export class ReversiGameEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packDetailMany(
|
||||
xs: MiReversiGame[],
|
||||
public async packDetailMany(
|
||||
games: MiReversiGame[],
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packDetail(x)));
|
||||
const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
|
||||
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
|
||||
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
games.map(game => {
|
||||
return this.packDetail(game, {
|
||||
packedUser1: _userMap.get(game.user1Id),
|
||||
packedUser2: _userMap.get(game.user2Id),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
hint?: {
|
||||
packedUser1?: Packed<'UserLite'>,
|
||||
packedUser2?: Packed<'UserLite'>,
|
||||
},
|
||||
): Promise<Packed<'ReversiGameLite'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await Promise.all([
|
||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||
]);
|
||||
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
|
||||
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
|
@ -94,10 +109,10 @@ export class ReversiGameEntityService {
|
|||
isEnded: game.isEnded,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
user1,
|
||||
user2,
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
|
@ -111,10 +126,21 @@ export class ReversiGameEntityService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public packLiteMany(
|
||||
xs: MiReversiGame[],
|
||||
public async packLiteMany(
|
||||
games: MiReversiGame[],
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packLite(x)));
|
||||
const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
|
||||
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
|
||||
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(
|
||||
games.map(game => {
|
||||
return this.packLite(game, {
|
||||
packedUser1: _userMap.get(game.user1Id),
|
||||
packedUser2: _userMap.get(game.user2Id),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,11 +50,14 @@ export class UserListEntityService {
|
|||
public async packMembershipsMany(
|
||||
memberships: MiUserListMembership[],
|
||||
) {
|
||||
const _users = memberships.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(memberships.map(async x => ({
|
||||
id: x.id,
|
||||
createdAt: this.idService.parse(x.id).date.toISOString(),
|
||||
userId: x.userId,
|
||||
user: await this.userEntityService.pack(x.userId),
|
||||
user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
|
||||
withReplies: x.withReplies,
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@ export type SchemaTypeDef<p extends Schema> =
|
|||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
|
|
|
@ -90,9 +90,6 @@ export class MiAntenna {
|
|||
})
|
||||
public expression: string | null;
|
||||
|
||||
@Column('boolean')
|
||||
public notify: boolean;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
|
|
|
@ -81,13 +81,22 @@ export class MiInstance {
|
|||
public isNotResponding: boolean;
|
||||
|
||||
/**
|
||||
* このインスタンスへの配信を停止するか
|
||||
* このインスタンスと不通になった日時
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public notRespondingSince: Date | null;
|
||||
|
||||
/**
|
||||
* このインスタンスへの配信状態
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
@Column('enum', {
|
||||
default: 'none',
|
||||
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
|
||||
})
|
||||
public isSuspended: boolean;
|
||||
public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
|
||||
|
||||
@Column('varchar', {
|
||||
length: 64, nullable: true,
|
||||
|
|
|
@ -403,6 +403,12 @@ export class MiMeta {
|
|||
})
|
||||
public donationUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public inquiryUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 8192,
|
||||
nullable: true,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { noteVisibilities } from '@/types.js';
|
|||
import { id } from './util/id.js';
|
||||
import { MiNote } from './Note.js';
|
||||
import type { MiUser } from './User.js';
|
||||
import type { MiChannel } from "@/models/Channel.js";
|
||||
|
||||
@Entity('poll')
|
||||
export class MiPoll {
|
||||
|
@ -58,6 +59,14 @@ export class MiPoll {
|
|||
comment: '[Denormalized]',
|
||||
})
|
||||
public userHost: string | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public channelId: MiChannel['id'] | null;
|
||||
//#endregion
|
||||
|
||||
constructor(data: Partial<MiPoll>) {
|
||||
|
|
|
@ -72,10 +72,6 @@ export const packedAntennaSchema = {
|
|||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
notify: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
excludeBots: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -99,5 +95,10 @@ export const packedAntennaSchema = {
|
|||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
notify: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
suspensionState: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
|
||||
},
|
||||
isBlocked: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -243,6 +243,10 @@ export const packedMetaLiteSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
inquiryUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
serverRules: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { InstancesRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
@ -62,7 +63,7 @@ export class DeliverProcessorService {
|
|||
if (suspendedHosts == null) {
|
||||
suspendedHosts = await this.instancesRepository.find({
|
||||
where: {
|
||||
isSuspended: true,
|
||||
suspensionState: Not('none'),
|
||||
},
|
||||
});
|
||||
this.suspendedHostsCache.set(suspendedHosts);
|
||||
|
@ -79,6 +80,7 @@ export class DeliverProcessorService {
|
|||
if (i.isNotResponding) {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
isNotResponding: false,
|
||||
notRespondingSince: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -98,7 +100,15 @@ export class DeliverProcessorService {
|
|||
if (!i.isNotResponding) {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
isNotResponding: true,
|
||||
notRespondingSince: new Date(),
|
||||
});
|
||||
} else if (i.notRespondingSince) {
|
||||
// 1週間以上不通ならサスペンド
|
||||
if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
suspensionState: 'autoSuspendedForNotResponding',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.apRequestChart.deliverFail();
|
||||
|
@ -116,7 +126,7 @@ export class DeliverProcessorService {
|
|||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
isSuspended: true,
|
||||
suspensionState: 'goneSuspended',
|
||||
});
|
||||
});
|
||||
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||
|
|
|
@ -84,7 +84,6 @@ export class ExportAntennasProcessorService {
|
|||
excludeBots: antenna.excludeBots,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
notify: antenna.notify,
|
||||
}));
|
||||
if (antennas.length - 1 !== index) {
|
||||
write(', ');
|
||||
|
|
|
@ -47,9 +47,8 @@ const validate = new Ajv().compile({
|
|||
excludeBots: { type: 'boolean' },
|
||||
withReplies: { type: 'boolean' },
|
||||
withFile: { type: 'boolean' },
|
||||
notify: { type: 'boolean' },
|
||||
},
|
||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
|
@ -92,7 +91,6 @@ export class ImportAntennasProcessorService {
|
|||
excludeBots: antenna.excludeBots,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
notify: antenna.notify,
|
||||
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.logger.succ('Antenna created: ' + result.id);
|
||||
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
||||
|
|
|
@ -193,6 +193,8 @@ export class InboxProcessorService {
|
|||
this.federatedInstanceService.update(i.id, {
|
||||
latestRequestReceivedAt: new Date(),
|
||||
isNotResponding: false,
|
||||
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
|
||||
suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
|
||||
});
|
||||
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
|
@ -207,13 +209,22 @@ export class InboxProcessorService {
|
|||
|
||||
// アクティビティを処理
|
||||
try {
|
||||
await this.apInboxService.performActivity(authUser.user, activity);
|
||||
const result = await this.apInboxService.performActivity(authUser.user, activity);
|
||||
if (result && !result.startsWith('ok')) {
|
||||
this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||
return 'blocked notes with prohibited words';
|
||||
}
|
||||
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
|
||||
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
|
||||
return 'actor has been suspended';
|
||||
}
|
||||
if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
|
54
packages/backend/src/server/HealthServerService.ts
Normal file
54
packages/backend/src/server/HealthServerService.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { readyRef } from '@/boot/ready.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
import type { MeiliSearch } from 'meilisearch';
|
||||
|
||||
@Injectable()
|
||||
export class HealthServerService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redis: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForPub)
|
||||
private redisForPub: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.meilisearch)
|
||||
private meilisearch: MeiliSearch | null,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
reply.code(await Promise.all([
|
||||
new Promise<void>((resolve, reject) => readyRef.value ? resolve() : reject()),
|
||||
this.redis.ping(),
|
||||
this.redisForPub.ping(),
|
||||
this.redisForSub.ping(),
|
||||
this.redisForTimelines.ping(),
|
||||
this.db.query('SELECT 1'),
|
||||
...(this.meilisearch ? [this.meilisearch.health()] : []),
|
||||
]).then(() => 200, () => 503));
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
|
@ -36,12 +36,12 @@ export class NodeinfoServerService {
|
|||
@bindThis
|
||||
public getLinks() {
|
||||
return [{
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||
href: this.config.url + nodeinfo2_1path
|
||||
}, {
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
href: this.config.url + nodeinfo2_0path,
|
||||
}];
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||
href: this.config.url + nodeinfo2_1path,
|
||||
}, {
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
href: this.config.url + nodeinfo2_0path,
|
||||
}];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -107,6 +107,7 @@ export class NodeinfoServerService {
|
|||
langs: meta.langs,
|
||||
tosUrl: meta.termsOfServiceUrl,
|
||||
privacyPolicyUrl: meta.privacyPolicyUrl,
|
||||
inquiryUrl: meta.inquiryUrl,
|
||||
impressumUrl: meta.impressumUrl,
|
||||
donationUrl: meta.donationUrl,
|
||||
repositoryUrl: meta.repositoryUrl,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
|||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ServerService } from './ServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
|
@ -58,6 +59,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
|
|||
ClientServerService,
|
||||
ClientLoggerService,
|
||||
FeedService,
|
||||
HealthServerService,
|
||||
UrlPreviewService,
|
||||
ActivityPubServerService,
|
||||
FileServerService,
|
||||
|
|
|
@ -28,6 +28,7 @@ import { ApiServerService } from './api/ApiServerService.js';
|
|||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
|
||||
|
@ -63,6 +64,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
private wellKnownServerService: WellKnownServerService,
|
||||
private nodeinfoServerService: NodeinfoServerService,
|
||||
private fileServerService: FileServerService,
|
||||
private healthServerService: HealthServerService,
|
||||
private clientServerService: ClientServerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private loggerService: LoggerService,
|
||||
|
@ -110,6 +112,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
fastify.register(this.nodeinfoServerService.createServer);
|
||||
fastify.register(this.wellKnownServerService.createServer);
|
||||
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
|
||||
fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
|
||||
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
|||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream/promises';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
|
@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
|
@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.userIpsRepository)
|
||||
private userIpsRepository: UserIpsRepository,
|
||||
|
||||
|
@ -88,6 +93,48 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
#onExecError(ep: IEndpoint, data: any, err: Error): void {
|
||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
} else {
|
||||
const errId = randomUUID();
|
||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
});
|
||||
console.error(err, errId);
|
||||
|
||||
if (this.config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
extra: {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new ApiError(null, {
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
id: errId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public handleRequest(
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
|
@ -362,31 +409,11 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// API invoking
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
|
||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
} else {
|
||||
const errId = randomUUID();
|
||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
});
|
||||
console.error(err, errId);
|
||||
throw new ApiError(null, {
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
id: errId,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
if (this.config.sentryForBackend) {
|
||||
return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
|
||||
} else {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -137,7 +137,7 @@ export class ApiServerService {
|
|||
const instances = await this.instancesRepository.find({
|
||||
select: ['host'],
|
||||
where: {
|
||||
isSuspended: false,
|
||||
suspensionState: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
|
|||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
import * as ep___antennas_list from './endpoints/antennas/list.js';
|
||||
|
@ -473,6 +474,7 @@ const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', us
|
|||
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
|
||||
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
|
||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
||||
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
|
||||
const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
|
||||
|
@ -862,6 +864,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_roles_updateDefaultPolicies,
|
||||
$admin_roles_users,
|
||||
$announcements,
|
||||
$announcements_show,
|
||||
$antennas_create,
|
||||
$antennas_delete,
|
||||
$antennas_list,
|
||||
|
@ -1245,6 +1248,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_roles_updateDefaultPolicies,
|
||||
$admin_roles_users,
|
||||
$announcements,
|
||||
$announcements_show,
|
||||
$antennas_create,
|
||||
$antennas_delete,
|
||||
$antennas_list,
|
||||
|
|
|
@ -88,6 +88,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
|
|||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
import * as ep___antennas_list from './endpoints/antennas/list.js';
|
||||
|
@ -471,6 +472,7 @@ const eps = [
|
|||
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
||||
['admin/roles/users', ep___admin_roles_users],
|
||||
['announcements', ep___announcements],
|
||||
['announcements/show', ep___announcements_show],
|
||||
['antennas/create', ep___antennas_create],
|
||||
['antennas/delete', ep___antennas_delete],
|
||||
['antennas/list', ep___antennas_list],
|
||||
|
|
|
@ -47,13 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new Error('instance not found');
|
||||
}
|
||||
|
||||
const isSuspendedBefore = instance.suspensionState !== 'none';
|
||||
let suspensionState: undefined | 'manuallySuspended' | 'none';
|
||||
|
||||
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
|
||||
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
|
||||
}
|
||||
|
||||
await this.federatedInstanceService.update(instance.id, {
|
||||
isSuspended: ps.isSuspended,
|
||||
suspensionState,
|
||||
isNSFW: ps.isNSFW,
|
||||
moderationNote: ps.moderationNote,
|
||||
});
|
||||
|
||||
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
|
||||
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
|
||||
if (ps.isSuspended) {
|
||||
this.moderationLogService.log(me, 'suspendRemoteInstance', {
|
||||
id: instance.id,
|
||||
|
|
|
@ -458,6 +458,10 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
inquiryUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
repositoryUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
@ -545,6 +549,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
impressumUrl: instance.impressumUrl,
|
||||
donationUrl: instance.donationUrl,
|
||||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||
|
|
|
@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
const _users = assigns.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return await Promise.all(assigns.map(async assign => ({
|
||||
id: assign.id,
|
||||
createdAt: this.idService.parse(assign.id).date.toISOString(),
|
||||
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
|
||||
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
|
||||
expiresAt: assign.expiresAt?.toISOString() ?? null,
|
||||
})));
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:show-users',
|
||||
kind: 'read:admin:show-user',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -113,6 +113,7 @@ export const paramDef = {
|
|||
impressumUrl: { type: 'string', nullable: true },
|
||||
donationUrl: { type: 'string', nullable: true },
|
||||
privacyPolicyUrl: { type: 'string', nullable: true },
|
||||
inquiryUrl: { type: 'string', nullable: true },
|
||||
useObjectStorage: { type: 'boolean' },
|
||||
objectStorageBaseUrl: { type: 'string', nullable: true },
|
||||
objectStorageBucket: { type: 'string', nullable: true },
|
||||
|
@ -430,6 +431,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.privacyPolicyUrl = ps.privacyPolicyUrl;
|
||||
}
|
||||
|
||||
if (ps.inquiryUrl !== undefined) {
|
||||
set.inquiryUrl = ps.inquiryUrl;
|
||||
}
|
||||
|
||||
if (ps.useObjectStorage !== undefined) {
|
||||
set.useObjectStorage = ps.useObjectStorage;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
|
||||
import type { AnnouncementsRepository } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.announcementsRepository)
|
||||
private announcementsRepository: AnnouncementsRepository,
|
||||
|
||||
@Inject(DI.announcementReadsRepository)
|
||||
private announcementReadsRepository: AnnouncementReadsRepository,
|
||||
|
||||
private queryService: QueryService,
|
||||
private announcementService: AnnouncementService,
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
|
||||
|
@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const announcements = await query.limit(ps.limit).getMany();
|
||||
|
||||
return this.announcementService.packMany(announcements, me);
|
||||
return this.announcementEntityService.packMany(announcements, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityNotFoundError } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Announcement',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAnnouncement: {
|
||||
message: 'No such announcement.',
|
||||
code: 'NO_SUCH_ANNOUNCEMENT',
|
||||
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
announcementId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['announcementId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private announcementService: AnnouncementService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
return await this.announcementService.getAnnouncement(ps.announcementId, me);
|
||||
} catch (err) {
|
||||
if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -67,9 +67,8 @@ export const paramDef = {
|
|||
excludeBots: { type: 'boolean' },
|
||||
withReplies: { type: 'boolean' },
|
||||
withFile: { type: 'boolean' },
|
||||
notify: { type: 'boolean' },
|
||||
},
|
||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -128,7 +127,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
excludeBots: ps.excludeBots,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
|
||||
|
|
|
@ -66,7 +66,6 @@ export const paramDef = {
|
|||
excludeBots: { type: 'boolean' },
|
||||
withReplies: { type: 'boolean' },
|
||||
withFile: { type: 'boolean' },
|
||||
notify: { type: 'boolean' },
|
||||
},
|
||||
required: ['antennaId'],
|
||||
} as const;
|
||||
|
@ -124,7 +123,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
excludeBots: ps.excludeBots,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
folderId: ps.folderId ?? IsNull(),
|
||||
});
|
||||
|
||||
return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
|
||||
return await this.driveFileEntityService.packMany(files, { self: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
|
||||
return await this.followRequestEntityService.packMany(requests, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { In } from 'typeorm';
|
|||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
|
||||
import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||
|
@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
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) + (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);
|
||||
let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
|
||||
let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let notifications: MiNotification[];
|
||||
for (;;) {
|
||||
let notificationsRes: [id: string, fields: string[]][];
|
||||
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
|
||||
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
|
||||
if (sinceTime && !untilTime) {
|
||||
notificationsRes = await this.redisClient.xrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
'(' + sinceTime,
|
||||
'+',
|
||||
'COUNT', ps.limit);
|
||||
} else {
|
||||
notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
untilTime ? '(' + untilTime : '+',
|
||||
sinceTime ? '(' + sinceTime : '-',
|
||||
'COUNT', ps.limit);
|
||||
}
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (notifications.length === 0) {
|
||||
return [];
|
||||
notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
|
||||
if (notifications.length !== 0) {
|
||||
// 通知が1件以上ある場合は返す
|
||||
break;
|
||||
}
|
||||
|
||||
// フィルタしたことで通知が0件になった場合、次のページを取得する
|
||||
if (ps.sinceId && !ps.untilId) {
|
||||
sinceTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
} else {
|
||||
untilTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all as read
|
||||
|
|
|
@ -530,26 +530,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private async verifyLink(url: string, user: MiLocalUser) {
|
||||
if (!safeForSql(url)) return;
|
||||
|
||||
const html = await this.httpRequestService.getHtml(url);
|
||||
try {
|
||||
const html = await this.httpRequestService.getHtml(url);
|
||||
|
||||
const { window } = new JSDOM(html);
|
||||
const doc = window.document;
|
||||
const { window } = new JSDOM(html);
|
||||
const doc = window.document;
|
||||
|
||||
const myLink = `${this.config.url}/@${user.username}`;
|
||||
const myLink = `${this.config.url}/@${user.username}`;
|
||||
|
||||
const aEls = Array.from(doc.getElementsByTagName('a'));
|
||||
const linkEls = Array.from(doc.getElementsByTagName('link'));
|
||||
const aEls = Array.from(doc.getElementsByTagName('a'));
|
||||
const linkEls = Array.from(doc.getElementsByTagName('link'));
|
||||
|
||||
const includesMyLink = aEls.some(a => a.href === myLink);
|
||||
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
|
||||
const includesMyLink = aEls.some(a => a.href === myLink);
|
||||
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
|
||||
|
||||
if (includesMyLink || includesRelMeLinks) {
|
||||
await this.userProfilesRepository.createQueryBuilder('profile').update()
|
||||
.where('userId = :userId', { userId: user.id })
|
||||
.set({
|
||||
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
|
||||
})
|
||||
.execute();
|
||||
if (includesMyLink || includesRelMeLinks) {
|
||||
await this.userProfilesRepository.createQueryBuilder('profile').update()
|
||||
.where('userId = :userId', { userId: user.id })
|
||||
.set({
|
||||
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
window.close();
|
||||
} catch (err) {
|
||||
// なにもしない
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export const paramDef = {
|
|||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
excludeChannels: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.setParameters(mutingQuery.getParameters());
|
||||
//#endregion
|
||||
|
||||
//#region exclude channels
|
||||
if (ps.excludeChannels) {
|
||||
query.andWhere('poll.channelId IS NULL');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const polls = await query
|
||||
.orderBy('poll.noteId', 'DESC')
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const reactions = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
|
||||
return await this.noteReactionEntityService.packMany(reactions, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
const _users = assigns.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return await Promise.all(assigns.map(async assign => ({
|
||||
id: assign.id,
|
||||
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
|
||||
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
|
||||
|
||||
// Extract top replied users
|
||||
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
|
||||
const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
|
||||
|
||||
// Make replies object (includes weights)
|
||||
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
|
||||
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
|
||||
weight: repliedUsers[user] / peak,
|
||||
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
|
||||
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
|
||||
weight: repliedUsers[userId] / peak,
|
||||
})));
|
||||
|
||||
return repliesObj;
|
||||
|
|
|
@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
// リクエストされた通りに並べ替え
|
||||
// 順番は保持されるけど数は減ってる可能性がある
|
||||
const _users: MiUser[] = [];
|
||||
for (const id of ps.userIds) {
|
||||
_users.push(users.find(x => x.id === id)!);
|
||||
const user = users.find(x => x.id === id);
|
||||
if (user != null) _users.push(user);
|
||||
}
|
||||
|
||||
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
|
||||
schema: 'UserDetailed',
|
||||
})));
|
||||
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return _users.map(u => _userMap.get(u.id)!);
|
||||
} else {
|
||||
// Lookup user
|
||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||
|
|
|
@ -456,7 +456,7 @@ export class ClientServerService {
|
|||
|
||||
//#endregion
|
||||
|
||||
const renderBase = async (reply: FastifyReply) => {
|
||||
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await reply.view('base', {
|
||||
|
@ -465,6 +465,7 @@ export class ClientServerService {
|
|||
title: meta.name ?? 'Misskey',
|
||||
desc: meta.description,
|
||||
...await this.generateCommonPugData(meta),
|
||||
...data,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -483,7 +484,9 @@ export class ClientServerService {
|
|||
};
|
||||
|
||||
// Atom
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
|
||||
fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
|
||||
if (request.params.user == null) return await renderBase(reply);
|
||||
|
||||
const feed = await getFeed(request.params.user);
|
||||
|
||||
if (feed) {
|
||||
|
@ -496,7 +499,9 @@ export class ClientServerService {
|
|||
});
|
||||
|
||||
// RSS
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
|
||||
fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
|
||||
if (request.params.user == null) return await renderBase(reply);
|
||||
|
||||
const feed = await getFeed(request.params.user);
|
||||
|
||||
if (feed) {
|
||||
|
@ -509,7 +514,9 @@ export class ClientServerService {
|
|||
});
|
||||
|
||||
// JSON
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
|
||||
fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
|
||||
if (request.params.user == null) return await renderBase(reply);
|
||||
|
||||
const feed = await getFeed(request.params.user);
|
||||
|
||||
if (feed) {
|
||||
|
@ -762,6 +769,18 @@ export class ClientServerService {
|
|||
});
|
||||
//#endregion
|
||||
|
||||
//region noindex pages
|
||||
// Tags
|
||||
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
|
||||
return await renderBase(reply, { noindex: true });
|
||||
});
|
||||
|
||||
// User with Tags
|
||||
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
|
||||
return await renderBase(reply, { noindex: true });
|
||||
});
|
||||
//endregion
|
||||
|
||||
fastify.get('/_info_card_', async (request, reply) => {
|
||||
const meta = await this.metaService.fetch(true);
|
||||
|
||||
|
|
|
@ -55,6 +55,9 @@ html
|
|||
block title
|
||||
= title || 'Sharkey'
|
||||
|
||||
if noindex
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
block desc
|
||||
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ describe('アンテナ', () => {
|
|||
excludeKeywords: [['']],
|
||||
keywords: [['keyword']],
|
||||
name: 'test',
|
||||
notify: false,
|
||||
src: 'all' as const,
|
||||
userListId: null,
|
||||
users: [''],
|
||||
|
@ -151,7 +150,6 @@ describe('アンテナ', () => {
|
|||
isActive: true,
|
||||
keywords: [['keyword']],
|
||||
name: 'test',
|
||||
notify: false,
|
||||
src: 'all',
|
||||
userListId: null,
|
||||
users: [''],
|
||||
|
@ -159,6 +157,7 @@ describe('アンテナ', () => {
|
|||
withReplies: false,
|
||||
excludeBots: false,
|
||||
localOnly: false,
|
||||
notify: false,
|
||||
};
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
@ -219,8 +218,6 @@ describe('アンテナ', () => {
|
|||
{ parameters: () => ({ withReplies: true }) },
|
||||
{ parameters: () => ({ withFile: false }) },
|
||||
{ parameters: () => ({ withFile: true }) },
|
||||
{ parameters: () => ({ notify: false }) },
|
||||
{ parameters: () => ({ notify: true }) },
|
||||
];
|
||||
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
|
||||
const response = await successfulApiCall({
|
||||
|
|
|
@ -191,7 +191,6 @@ describe('Account Move', () => {
|
|||
localOnly: false,
|
||||
withReplies: false,
|
||||
withFile: false,
|
||||
notify: false,
|
||||
}, alice);
|
||||
antennaId = antenna.body.id;
|
||||
|
||||
|
@ -435,7 +434,6 @@ describe('Account Move', () => {
|
|||
localOnly: false,
|
||||
withReplies: false,
|
||||
withFile: false,
|
||||
notify: false,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 403);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ModuleMocker } from 'jest-mock';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import type {
|
||||
AnnouncementReadsRepository,
|
||||
AnnouncementsRepository,
|
||||
|
@ -67,6 +68,7 @@ describe('AnnouncementService', () => {
|
|||
],
|
||||
providers: [
|
||||
AnnouncementService,
|
||||
AnnouncementEntityService,
|
||||
CacheService,
|
||||
IdService,
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue