};
type ObjectSchemaTypeDef =
p['ref'] extends keyof typeof refs ? Packed
:
@@ -232,6 +235,12 @@ export type SchemaTypeDef
=
p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
never
) :
+ p['prefixItems'] extends ReadonlyArray ? (
+ p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType
[]] :
+ p['items'] extends false ? ArrayToTuple
:
+ p['unevaluatedItems'] extends false ? ArrayToTuple
:
+ [...ArrayToTuple
, ...unknown[]]
+ ) :
p['items'] extends NonNullable ? SchemaType[] :
any[]
) :
diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index dd810681c5..12d7b31e00 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -60,8 +60,7 @@ export class MiDriveFile {
})
public size: number;
- @Column('varchar', {
- length: 8192,
+ @Column('text', {
nullable: true,
comment: 'The comment of the DriveFile.',
})
diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts
index dd625f95d3..ba93190c57 100644
--- a/packages/backend/src/models/Instance.ts
+++ b/packages/backend/src/models/Instance.ts
@@ -158,7 +158,12 @@ export class MiInstance {
default: false,
})
public isNSFW: boolean;
-
+
+ @Column('boolean', {
+ default: false,
+ })
+ public rejectReports: boolean;
+
@Column('varchar', {
length: 16384, default: '',
})
diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts
new file mode 100644
index 0000000000..064fcccc0a
--- /dev/null
+++ b/packages/backend/src/models/LatestNote.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { MiUser } from '@/models/User.js';
+import { MiNote } from '@/models/Note.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
+
+/**
+ * Maps a user to the most recent post by that user.
+ * Public, home-only, and followers-only posts are included.
+ * DMs are not counted.
+ */
+@Entity('latest_note')
+export class SkLatestNote {
+ @PrimaryColumn({
+ name: 'user_id',
+ type: 'varchar' as const,
+ length: 32,
+ })
+ public userId: string;
+
+ @PrimaryColumn('boolean', {
+ name: 'is_public',
+ default: false,
+ })
+ public isPublic: boolean;
+
+ @PrimaryColumn('boolean', {
+ name: 'is_reply',
+ default: false,
+ })
+ public isReply: boolean;
+
+ @PrimaryColumn('boolean', {
+ name: 'is_quote',
+ default: false,
+ })
+ public isQuote: boolean;
+
+ @ManyToOne(() => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'user_id',
+ })
+ public user: MiUser | null;
+
+ @Column({
+ name: 'note_id',
+ type: 'varchar' as const,
+ length: 32,
+ })
+ public noteId: string;
+
+ @ManyToOne(() => MiNote, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'note_id',
+ })
+ public note: MiNote | null;
+
+ constructor(data?: Partial) {
+ if (!data) return;
+
+ for (const [k, v] of Object.entries(data)) {
+ (this as Record)[k] = v;
+ }
+ }
+
+ /**
+ * Generates a compound key matching a provided note.
+ */
+ static keyFor(note: MiNote) {
+ return {
+ userId: note.userId,
+ isPublic: note.visibility === 'public',
+ isReply: note.replyId != null,
+ isQuote: isRenote(note) && isQuote(note),
+ };
+ }
+
+ /**
+ * Checks if two notes would produce equivalent compound keys.
+ */
+ static areEquivalent(first: MiNote, second: MiNote): boolean {
+ const firstKey = SkLatestNote.keyFor(first);
+ const secondKey = SkLatestNote.keyFor(second);
+
+ return (
+ firstKey.userId === secondKey.userId &&
+ firstKey.isPublic === secondKey.isPublic &&
+ firstKey.isReply === secondKey.isReply &&
+ firstKey.isQuote === secondKey.isQuote
+ );
+ }
+}
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 07c4e28b3a..0ea6765d6a 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -139,6 +139,12 @@ export class MiMeta {
})
public app512IconUrl: string | null;
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public sidebarLogoUrl: string | null;
+
@Column('varchar', {
length: 1024,
nullable: true,
@@ -263,6 +269,23 @@ export class MiMeta {
})
public turnstileSecretKey: string | null;
+ @Column('boolean', {
+ default: false,
+ })
+ public enableFC: boolean;
+
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public fcSiteKey: string | null;
+
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public fcSecretKey: string | null;
+
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('enum', {
@@ -621,6 +644,11 @@ export class MiMeta {
})
public perUserListTimelineCacheMax: number;
+ @Column('boolean', {
+ default: false,
+ })
+ public enableReactionsBuffering: boolean;
+
@Column('integer', {
default: 0,
})
@@ -668,4 +696,25 @@ export class MiMeta {
nullable: true,
})
public urlPreviewUserAgent: string | null;
+
+ @Column('varchar', {
+ length: 3072,
+ array: true,
+ default: '{}',
+ comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.',
+ })
+ public trustedLinkUrlPatterns: string[];
+
+ @Column('varchar', {
+ length: 128,
+ default: 'all',
+ })
+ public federation: 'all' | 'specified' | 'none';
+
+ @Column('varchar', {
+ length: 1024,
+ array: true,
+ default: '{}',
+ })
+ public federationHosts: string[];
}
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index b11e2ec62b..408e023ff7 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -66,8 +66,8 @@ export class MiNote {
})
public name: string | null;
- @Column('varchar', {
- length: 512, nullable: true,
+ @Column('text', {
+ nullable: true,
})
public cw: string | null;
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index 4ed71a106c..c4f046c565 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -7,6 +7,8 @@ import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
+import { MiDriveFile } from './DriveFile.js';
+import { userExportableEntities } from '@/types.js';
export type MiNotification = {
type: 'note';
@@ -67,6 +69,7 @@ export type MiNotification = {
id: string;
createdAt: string;
notifierId: MiUser['id'];
+ message: string | null;
} | {
type: 'roleAssigned';
id: string;
@@ -77,6 +80,12 @@ export type MiNotification = {
id: string;
createdAt: string;
achievement: string;
+} | {
+ type: 'exportCompleted';
+ id: string;
+ createdAt: string;
+ exportedEntity: typeof userExportableEntities[number];
+ fileId: MiDriveFile['id'];
} | {
type: 'app';
id: string;
@@ -85,7 +94,7 @@ export type MiNotification = {
/**
* アプリ通知のbody
*/
- customBody: string | null;
+ customBody: string;
/**
* アプリ通知のheader
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 1eaeb86df6..eb45b9a631 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -7,6 +7,7 @@ import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import {
+ SkLatestNote,
MiAbuseReportNotificationRecipient,
MiAbuseUserReport,
MiAccessToken,
@@ -118,6 +119,12 @@ const $avatarDecorationsRepository: Provider = {
inject: [DI.db],
};
+const $latestNotesRepository: Provider = {
+ provide: DI.latestNotesRepository,
+ useFactory: (db: DataSource) => db.getRepository(SkLatestNote).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository),
@@ -511,6 +518,7 @@ const $reversiGamesRepository: Provider = {
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
+ $latestNotesRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
@@ -583,6 +591,7 @@ const $reversiGamesRepository: Provider = {
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
+ $latestNotesRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index b0910133c9..c7ecccf1cf 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -170,6 +170,7 @@ export class MiUser {
flipH?: boolean;
offsetX?: number;
offsetY?: number;
+ showBelow?: boolean;
}[];
@Index()
@@ -178,6 +179,11 @@ export class MiUser {
})
public tags: string[];
+ @Column('integer', {
+ default: 0,
+ })
+ public score: number;
+
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.',
@@ -340,6 +346,7 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
export const passwordSchema = { type: 'string', minLength: 1 } as const;
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
+export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const;
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 40ea26f610..751b1aff08 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -49,6 +49,14 @@ export class MiUserProfile {
})
public description: string | null;
+ // フォローされた際のメッセージ
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public followedMessage: string | null;
+
+ // TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
+
@Column('jsonb', {
default: [],
})
@@ -188,6 +196,11 @@ export class MiUserProfile {
})
public alwaysMarkNsfw: boolean;
+ @Column('boolean', {
+ default: false,
+ })
+ public defaultSensitive: boolean;
+
@Column('boolean', {
default: false,
})
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index 2a727f86fd..8ef73fa143 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,6 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
+export type WebhookEventTypes = typeof webhookEventTypes[number];
@Entity('webhook')
export class MiWebhook {
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index f7646dce2a..ac2dd62aa2 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -10,6 +10,7 @@ import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLo
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
+import { SkLatestNote } from '@/models/LatestNote.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
@@ -126,6 +127,7 @@ export const miRepository = {
} satisfies MiRepository;
export {
+ SkLatestNote,
MiAbuseUserReport,
MiAbuseReportNotificationRecipient,
MiAccessToken,
@@ -224,6 +226,7 @@ export type GalleryPostsRepository = Repository & MiRepository & MiRepository;
export type InstancesRepository = Repository & MiRepository;
export type MetasRepository = Repository & MiRepository;
+export type LatestNotesRepository = Repository & MiRepository;
export type ModerationLogsRepository = Repository & MiRepository;
export type MutingsRepository = Repository & MiRepository;
export type RenoteMutingsRepository = Repository & MiRepository;
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index 062dba9bad..7960e748e9 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -121,6 +121,11 @@ export const packedFederationInstanceSchema = {
optional: false,
nullable: false,
},
+ rejectReports: {
+ type: 'boolean',
+ optional: false,
+ nullable: false,
+ },
moderationNote: {
type: 'string',
optional: true, nullable: true,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 1d620f16fd..decdbd5650 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -127,6 +127,14 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
+ enableFC: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ fcSiteKey: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableAchievements: {
type: 'boolean',
optional: false, nullable: true,
@@ -160,10 +168,34 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
+ sidebarLogoUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
+ maxRemoteNoteTextLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ maxCwLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ maxRemoteCwLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ maxAltTextLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ maxRemoteAltTextLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
ads: {
type: 'array',
optional: false, nullable: false,
@@ -269,6 +301,18 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
default: 'local',
},
+ trustedLinkUrlPatterns: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ maxFileSize: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 3f31cc47ee..990e8957cf 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { notificationTypes } from '@/types.js';
+import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
+import { notificationTypes, userExportableEntities } from '@/types.js';
const baseSchema = {
type: 'object',
@@ -266,6 +267,10 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
format: 'id',
},
+ message: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
},
}, {
type: 'object',
@@ -294,6 +299,27 @@ export const packedNotificationSchema = {
achievement: {
type: 'string',
optional: false, nullable: false,
+ enum: ACHIEVEMENT_TYPES,
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['exportCompleted'],
+ },
+ exportedEntity: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: userExportableEntities,
+ },
+ fileId: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
},
},
}, {
@@ -311,11 +337,11 @@ export const packedNotificationSchema = {
},
header: {
type: 'string',
- optional: false, nullable: false,
+ optional: false, nullable: true,
},
icon: {
type: 'string',
- optional: false, nullable: false,
+ optional: false, nullable: true,
},
},
}, {
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 504b9b122f..19ea6263c9 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -276,6 +276,26 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
+ canImportAntennas: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportBlocking: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportFollowing: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportMuting: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportUserLists: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 33a3efd453..d5e847cc40 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -104,6 +104,10 @@ export const packedUserLiteSchema = {
type: 'number',
nullable: false, optional: true,
},
+ showBelow: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
},
},
},
@@ -117,9 +121,10 @@ export const packedUserLiteSchema = {
nullable: false, optional: true,
default: false,
},
- isSilenced: {
+ isSystem: {
type: 'boolean',
- nullable: false, optional: false,
+ nullable: false, optional: true,
+ default: false,
},
noindex: {
type: 'boolean',
@@ -137,6 +142,10 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
+ isSilenced: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
instance: {
type: 'object',
nullable: false, optional: true,
@@ -268,6 +277,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ isSilenced: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
isSuspended: {
type: 'boolean',
nullable: false, optional: false,
@@ -287,7 +300,7 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: true, optional: false,
example: '2018-03-12',
},
- ListenBrainz: {
+ listenbrainz: {
type: 'string',
nullable: true,
optional: false,
@@ -403,6 +416,10 @@ export const packedUserDetailedNotMeOnlySchema = {
ref: 'RoleLite',
},
},
+ followedMessage: {
+ type: 'string',
+ nullable: true, optional: true,
+ },
memo: {
type: 'string',
nullable: true, optional: false,
@@ -475,6 +492,10 @@ export const packedMeDetailedOnlySchema = {
nullable: true, optional: false,
format: 'id',
},
+ followedMessage: {
+ type: 'string',
+ nullable: true, optional: false,
+ },
isModerator: {
type: 'boolean',
nullable: true, optional: false,
@@ -495,6 +516,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ defaultSensitive: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
autoSensitive: {
type: 'boolean',
nullable: false, optional: false,
@@ -569,6 +594,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ hasPendingSentFollowRequest: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
unreadNotificationsCount: {
type: 'number',
nullable: false, optional: false,
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 047b7f8ae9..2d66e6e445 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -83,6 +83,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { SkLatestNote } from '@/models/LatestNote.js';
pg.types.setTypeParser(20, Number);
@@ -130,6 +131,7 @@ class MyCustomLogger implements Logger {
}
export const entities = [
+ SkLatestNote,
MiAnnouncement,
MiAnnouncementRead,
MiMeta,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 7daca687a1..7c6675b15d 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -14,6 +14,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
@@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ResyncChartsProcessorService,
CleanChartsProcessorService,
CheckExpiredMutingsProcessorService,
+ BakeBufferedReactionsProcessorService,
CleanProcessorService,
DeleteDriveFilesProcessorService,
ExportAccountDataProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 7a6169bf9c..eaeb6d58df 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -40,6 +40,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
@@ -122,24 +123,35 @@ export class QueueProcessorService implements OnApplicationShutdown {
private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
+ private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
- function renderError(e: Error): any {
- if (e) { // 何故かeがundefinedで来ることがある
- return {
- stack: e.stack,
- message: e.message,
- name: e.name,
- };
- } else {
- return {
- stack: '?',
- message: '?',
- name: '?',
- };
+ function renderError(e?: Error) {
+ // 何故かeがundefinedで来ることがある
+ if (!e) return '?';
+
+ if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
+ return `${e.name}: ${e.message}`;
}
+
+ return {
+ stack: e.stack,
+ message: e.message,
+ name: e.name,
+ };
+ }
+
+ function renderJob(job?: Bull.Job) {
+ if (!job) return '?';
+
+ return {
+ name: job.name || undefined,
+ info: getJobInfo(job),
+ failedReason: job.failedReason || undefined,
+ data: job.data,
+ };
}
//#region system
@@ -151,6 +163,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'cleanCharts': return this.cleanChartsProcessorService.process();
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
@@ -173,15 +186,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
- logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
+ Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -238,15 +251,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
+ Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -278,15 +291,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
+ Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -318,15 +331,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
+ Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -358,15 +371,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
+ Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -398,15 +411,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
+ Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -445,15 +458,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
+ Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -486,15 +499,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
+ Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
new file mode 100644
index 0000000000..d49c99f694
--- /dev/null
+++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+
+@Injectable()
+export class BakeBufferedReactionsProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
+ private reactionsBufferingService: ReactionsBufferingService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
+ }
+
+ @bindThis
+ public async process(): Promise {
+ if (!this.meta.enableReactionsBuffering) {
+ this.logger.info('Reactions buffering is disabled. Skipping...');
+ return;
+ }
+
+ this.logger.info('Baking buffered reactions...');
+
+ await this.reactionsBufferingService.bake();
+
+ this.logger.succ('All buffered reactions baked.');
+ }
+}
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 4076e9da90..9590a4fe71 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -7,9 +7,8 @@ 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 { InstancesRepository, MiMeta } from '@/models/_.js';
import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
@@ -31,10 +30,12 @@ export class DeliverProcessorService {
private latest: string | null;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
- private metaService: MetaService,
private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -52,9 +53,7 @@ export class DeliverProcessorService {
public async process(job: Bull.Job): Promise {
const { host } = new URL(job.data.to);
- // ブロックしてたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
+ if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
return 'skip (blocked)';
}
@@ -88,7 +87,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverSucc();
this.federationChart.deliverd(i.host, true);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true);
}
});
@@ -120,7 +119,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false);
}
});
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index 88c4ea29c0..b3111865ad 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DBExportAntennasData } from '../types.js';
import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
private driveService: DriveService,
private utilityService: UtilityService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
}
@@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ('Exported to: ' + driveFile.id);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'antenna',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index 6ec3c18786..ecc439db69 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
private blockingsRepository: BlockingsRepository,
private utilityService: UtilityService,
+ private notificationService: NotificationService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
) {
@@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'blocking',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
index 01eab26e96..583ddbb745 100644
--- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
@@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
}
@@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'clip',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index 45087927a5..14d32e78b3 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
private driveService: DriveService,
private downloadService: DownloadService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
}
@@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'customEmoji',
+ fileId: driveFile.id,
+ });
+
cleanup();
archiveCleanup();
resolve();
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 7bb626dd31..b81feece01 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
}
@@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'favorite',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index 1cc80e66d7..903f962515 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiFollowing } from '@/models/Following.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
}
@@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'following',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index 243b74f2c2..f9867ade29 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
}
@@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'muting',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index c7611012d7..9e2b678219 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
import { FileWriterStream } from '@/misc/FileWriterStream.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
@@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
private queueLoggerService: QueueLoggerService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
}
@@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'note',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index ee87cff5d3..c483d79854 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportUserListsProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
}
@@ -89,6 +91,11 @@ export class ExportUserListsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'userList',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
index 04ad74ee01..17ba71df3d 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -88,23 +88,30 @@ export class ImportCustomEmojisProcessorService {
await this.emojisRepository.delete({
name: nameNfc,
});
- const driveFile = await this.driveService.addFile({
- user: null,
- path: emojiPath,
- name: record.fileName,
- force: true,
- });
- await this.customEmojiService.add({
- name: nameNfc,
- category: emojiInfo.category?.normalize('NFC'),
- host: null,
- aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
- driveFile,
- license: emojiInfo.license,
- isSensitive: emojiInfo.isSensitive,
- localOnly: emojiInfo.localOnly,
- roleIdsThatCanBeUsedThisEmojiAsReaction: [],
- });
+ try {
+ const driveFile = await this.driveService.addFile({
+ user: null,
+ path: emojiPath,
+ name: record.fileName,
+ force: true,
+ });
+ await this.customEmojiService.add({
+ name: nameNfc,
+ category: emojiInfo.category?.normalize('NFC'),
+ host: null,
+ aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
+ driveFile,
+ license: emojiInfo.license,
+ isSensitive: emojiInfo.isSensitive,
+ localOnly: emojiInfo.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: [],
+ });
+ } catch (e) {
+ if (e instanceof Error || typeof e === 'string') {
+ this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`);
+ }
+ continue;
+ }
}
cleanup();
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 641b8b8607..11b00bb683 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -4,11 +4,10 @@
*/
import { URL } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import httpSignature from '@peertube/http-signature';
import * as Bull from 'bullmq';
import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
@@ -26,16 +25,28 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
+import { MiNote } from '@/models/Note.js';
+import { MiMeta } from '@/models/Meta.js';
+import { DI } from '@/di-symbols.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { InboxJobData } from '../types.js';
+type UpdateInstanceJob = {
+ latestRequestReceivedAt: Date,
+ shouldUnsuspend: boolean,
+};
+
@Injectable()
-export class InboxProcessorService {
+export class InboxProcessorService implements OnApplicationShutdown {
private logger: Logger;
+ private updateInstanceQueue: CollapsedQueue;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private utilityService: UtilityService,
- private metaService: MetaService,
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -48,6 +59,7 @@ export class InboxProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
+ this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
}
@bindThis
@@ -63,9 +75,7 @@ export class InboxProcessorService {
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
- // ブロックしてたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
+ if (!this.utilityService.isFederationAllowedHost(host)) {
return `Blocked request: ${host}`;
}
@@ -108,19 +118,16 @@ export class InboxProcessorService {
// HTTP-Signatureの検証
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
- // また、signatureのsignerは、activity.actorと一致する必要がある
- if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
- let renewKeyFailed = true;
-
- if (!httpSignatureValidated) {
- authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
-
- if (authUser.key != null) {
- httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
- renewKeyFailed = false;
- }
+ // maybe they changed their key? refetch it
+ if (!httpSignatureValidated) {
+ authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
+ if (authUser.key != null) {
+ httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
}
+ }
+ // また、signatureのsignerは、activity.actorと一致する必要がある
+ if (!httpSignatureValidated || authUser.user.uri !== getApId(activity.actor)) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
const ldSignature = activity.signature;
if (ldSignature) {
@@ -169,9 +176,8 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
}
- // ブロックしてたら中断
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
- if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
+ if (!this.utilityService.isFederationAllowedHost(ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
}
} else {
@@ -190,11 +196,9 @@ export class InboxProcessorService {
// Update stats
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
- this.federatedInstanceService.update(i.id, {
+ this.updateInstanceQueue.enqueue(i.id, {
latestRequestReceivedAt: new Date(),
- isNotResponding: false,
- // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
- suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
+ shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
});
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
@@ -202,7 +206,7 @@ export class InboxProcessorService {
this.apRequestChart.inbox();
this.federationChart.inbox(i.host);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
});
@@ -230,4 +234,36 @@ export class InboxProcessorService {
}
return 'ok';
}
+
+ @bindThis
+ public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) {
+ const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt
+ ? newJob.latestRequestReceivedAt
+ : oldJob.latestRequestReceivedAt;
+ const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend;
+ return {
+ latestRequestReceivedAt,
+ shouldUnsuspend,
+ };
+ }
+
+ @bindThis
+ public async performUpdateInstance(id: string, job: UpdateInstanceJob) {
+ await this.federatedInstanceService.update(id, {
+ latestRequestReceivedAt: new Date(),
+ isNotResponding: false,
+ // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
+ suspensionState: job.shouldUnsuspend ? 'none' : undefined,
+ });
+ }
+
+ @bindThis
+ public async dispose(): Promise {
+ await this.updateInstanceQueue.performAllNow();
+ }
+
+ @bindThis
+ async onApplicationShutdown(signal?: string) {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 398ad54f6a..52592c47c6 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -21,7 +21,6 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { QueueService } from '@/core/QueueService.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
@@ -75,7 +74,6 @@ export class ActivityPubServerService {
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
- private metaService: MetaService,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private instanceActorService: InstanceActorService,
@@ -175,8 +173,7 @@ export class ActivityPubServerService {
return true;
}
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, keyHost)) {
+ if (!this.utilityService.isFederationAllowedHost(keyHost)) {
/* blocked instance: refuse (we don't care if the signature is
good, if they even pretend to be from a blocked instance,
they're out) */
@@ -208,15 +205,11 @@ export class ActivityPubServerService {
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+ // maybe they changed their key? refetch it
if (!httpSignatureValidated) {
- this.authlogger.info(`${logPrefix} failed to validate signature, re-fetching the key for ${authUser.user.uri}`);
- // maybe they changed their key? refetch it
authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user);
-
if (authUser.key != null) {
httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
- } else {
- this.authlogger.warn(`${logPrefix} failed to re-fetch key for ${authUser.user}`);
}
}
@@ -795,7 +788,7 @@ export class ActivityPubServerService {
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
-
+
vary(reply.raw, 'Accept');
const userId = request.params.user;
@@ -811,7 +804,7 @@ export class ActivityPubServerService {
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
-
+
vary(reply.raw, 'Accept');
const user = await this.usersRepository.findOneBy({
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 65a8218174..1a4d0cb48f 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -80,7 +80,7 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
- return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
+ return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
});
done();
});
@@ -145,12 +145,12 @@ export class FileServerService {
url.searchParams.set('static', '1');
file.cleanup();
- return await reply.redirect(301, url.toString());
+ return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
- return await reply.redirect(301, externalThumbnail);
+ return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@@ -165,7 +165,7 @@ export class FileServerService {
url.searchParams.set('url', file.url);
file.cleanup();
- return await reply.redirect(301, url.toString());
+ return await reply.redirect(url.toString(), 301);
}
}
@@ -312,11 +312,17 @@ export class FileServerService {
}
return await reply.redirect(
- 301,
url.toString(),
+ 301,
);
}
+ if (!request.headers['user-agent']) {
+ throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
+ } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
+ throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
+ }
+
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
index 2c3ed85925..5980609f02 100644
--- a/packages/backend/src/server/HealthServerService.ts
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -27,6 +27,9 @@ export class HealthServerService {
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
+ @Inject(DI.redisForReactions)
+ private redisForReactions: Redis.Redis,
+
@Inject(DI.db)
private db: DataSource,
@@ -43,6 +46,7 @@ export class HealthServerService {
this.redisForPub.ping(),
this.redisForSub.ping(),
this.redisForTimelines.ping(),
+ this.redisForReactions.ping(),
this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503));
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index bc8d3c0411..6dee6ecd78 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -121,7 +121,13 @@ export class NodeinfoServerService {
enableRecaptcha: meta.enableRecaptcha,
enableMcaptcha: meta.enableMcaptcha,
enableTurnstile: meta.enableTurnstile,
+ enableFC: meta.enableFC,
maxNoteTextLength: this.config.maxNoteLength,
+ maxRemoteNoteTextLength: this.config.maxRemoteNoteLength,
+ maxCwLength: this.config.maxCwLength,
+ maxRemoteCwLength: this.config.maxRemoteCwLength,
+ maxAltTextLength: this.config.maxAltTextLength,
+ maxRemoteAltTextLength: this.config.maxRemoteAltTextLength,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 39c8f67b8e..216e6b4fb8 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
+import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
imports: [
@@ -74,6 +75,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
AuthenticateService,
RateLimiterService,
SigninApiService,
+ SigninWithPasskeyApiService,
SigninService,
SignupApiService,
StreamingApiServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 30c133d9ec..43a2a3a2b0 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js';
-import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
@@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@@ -45,6 +44,9 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -54,7 +56,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
@@ -167,8 +168,8 @@ export class ServerService implements OnApplicationShutdown {
}
return await reply.redirect(
- 301,
url.toString(),
+ 301,
);
});
@@ -195,7 +196,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
- if ((await this.metaService.fetch()).enableIdenticonGeneration) {
+ if (this.meta.enableIdenticonGeneration) {
return await genIdenticon(request.params.x);
} else {
return reply.redirect('/static-assets/avatar.png');
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 808795fdac..016db6ac19 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import type Logger from '@/logger.js';
-import type { UserIpsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.config)
private config: Config,
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
- private metaService: MetaService,
private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService,
private roleService: RoleService,
@@ -199,9 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
- const [path] = await createTemp();
+ const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
+ // ファイルサイズが制限を超えていた場合
+ // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
+ if (multipartData.file.truncated) {
+ cleanup();
+ reply.code(413);
+ reply.send();
+ return;
+ }
+
const fields = {} as Record;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@@ -256,10 +266,14 @@ export class ApiCallService implements OnApplicationShutdown {
}
@bindThis
- private async logIp(request: FastifyRequest, user: MiLocalUser) {
- const meta = await this.metaService.fetch();
- if (!meta.enableIpLogging) return;
+ private logIp(request: FastifyRequest, user: MiLocalUser) {
+ if (!this.meta.enableIpLogging) return;
const ip = request.ip;
+ if (!ip) {
+ this.logger.warn(`user ${user.id} has a null IP address; please check your network configuration.`);
+ return;
+ }
+
const ips = this.userIpHistories.get(user.id);
if (ips == null || !ips.has(ip)) {
if (ips == null) {
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 4a5935f930..ac3b982742 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -8,6 +8,7 @@ import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import { ModuleRef } from '@nestjs/core';
+import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
@@ -17,6 +18,7 @@ import endpoints from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js';
+import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable()
@@ -37,6 +39,7 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
+ private signinWithPasskeyApiService: SigninWithPasskeyApiService,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -49,7 +52,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
- fileSize: this.config.maxFileSize ?? 262144000,
+ fileSize: this.config.maxFileSize,
files: 1,
},
});
@@ -115,6 +118,7 @@ export class ApiServerService {
'hcaptcha-response'?: string;
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
}
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
@@ -131,6 +135,12 @@ export class ApiServerService {
};
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+ fastify.post<{
+ Body: {
+ credential?: AuthenticationResponseJSON;
+ };
+ }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
+
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.get('/v1/instance/peers', async (request, reply) => {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 4a08410ceb..5bdd7cf650 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -79,6 +79,7 @@ import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
+import * as ep___admin_declineUser from './endpoints/admin/decline-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
@@ -97,6 +98,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.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';
@@ -189,6 +191,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
+import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@@ -266,6 +269,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -290,6 +294,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
+import * as ep___notes_following from './endpoints/notes/following.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
@@ -297,6 +302,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
+import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js';
import * as ep___notes_reactions from './endpoints/notes/reactions.js';
import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
@@ -475,6 +481,7 @@ const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClas
const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default };
+const $admin_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.default };
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
@@ -493,6 +500,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
+const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.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 };
@@ -585,6 +593,7 @@ const $following_invalidate: Provider = { provide: 'ep:following/invalidate', us
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
+const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
@@ -662,6 +671,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
+const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
@@ -686,6 +696,7 @@ const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___not
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
+const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default };
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default };
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
@@ -693,6 +704,7 @@ const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', use
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
+const $notes_polls_refresh: Provider = { provide: 'ep:notes/polls/refresh', useClass: ep___notes_polls_refresh.default };
const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default };
const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default };
const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default };
@@ -875,6 +887,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_unsilenceUser,
$admin_suspendUser,
$admin_approveUser,
+ $admin_declineUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_deleteAccount,
@@ -893,6 +906,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -985,6 +999,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$following_requests_accept,
$following_requests_cancel,
$following_requests_list,
+ $following_requests_sent,
$following_requests_reject,
$gallery_featured,
$gallery_popular,
@@ -1062,6 +1077,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@@ -1086,6 +1102,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
+ $notes_following,
$notes_globalTimeline,
$notes_bubbleTimeline,
$notes_hybridTimeline,
@@ -1093,6 +1110,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,
+ $notes_polls_refresh,
$notes_reactions,
$notes_reactions_create,
$notes_reactions_delete,
@@ -1269,6 +1287,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_unsilenceUser,
$admin_suspendUser,
$admin_approveUser,
+ $admin_declineUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_deleteAccount,
@@ -1287,6 +1306,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1456,6 +1476,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@@ -1480,6 +1501,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
+ $notes_following,
$notes_globalTimeline,
$notes_bubbleTimeline,
$notes_hybridTimeline,
@@ -1487,6 +1509,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_mentions,
$notes_polls_recommendation,
$notes_polls_vote,
+ $notes_polls_refresh,
$notes_reactions,
$notes_reactions_create,
$notes_reactions_delete,
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 6fbcacbc11..64af7da7a6 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -21,11 +21,12 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import { MetaService } from '@/core/MetaService.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
+import { isSystemAccount } from '@/misc/is-system-account.js';
+import type { MiMeta } from '@/models/_.js';
@Injectable()
export class SigninApiService {
@@ -33,6 +34,9 @@ export class SigninApiService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -47,7 +51,6 @@ export class SigninApiService {
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
- private metaService: MetaService,
) {
}
@@ -66,8 +69,6 @@ export class SigninApiService {
reply.header('Access-Control-Allow-Origin', this.config.url);
reply.header('Access-Control-Allow-Credentials', 'true');
- const instance = await this.metaService.fetch(true);
-
const body = request.body;
const username = body['username'];
const password = body['password'];
@@ -125,9 +126,15 @@ export class SigninApiService {
});
}
+ if (isSystemAccount(user)) {
+ return error(403, {
+ id: 's8dhsj9s-a93j-493j-ja9k-kas9sj20aml2',
+ });
+ }
+
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- if (!user.approved && instance.approvalRequiredForSignup) {
+ if (!user.approved && this.meta.approvalRequiredForSignup) {
reply.code(403);
return {
error: {
@@ -162,7 +169,7 @@ export class SigninApiService {
password: newHash
});
}
- if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
+ if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
return this.signinService.signin(request, reply, user);
} else {
@@ -193,7 +200,7 @@ export class SigninApiService {
});
}
- if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
+ if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
return this.signinService.signin(request, reply, user);
} else if (body.credential) {
@@ -206,7 +213,7 @@ export class SigninApiService {
const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
if (authorized) {
- if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
+ if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true });
return this.signinService.signin(request, reply, user);
} else {
return await fail(403, {
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
new file mode 100644
index 0000000000..9ba23c54e2
--- /dev/null
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -0,0 +1,173 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { randomUUID } from 'crypto';
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type {
+ SigninsRepository,
+ UserProfilesRepository,
+ UsersRepository,
+} from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { getIpHash } from '@/misc/get-ip-hash.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
+import { IdService } from '@/core/IdService.js';
+import { bindThis } from '@/decorators.js';
+import { WebAuthnService } from '@/core/WebAuthnService.js';
+import Logger from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type { IdentifiableError } from '@/misc/identifiable-error.js';
+import { RateLimiterService } from './RateLimiterService.js';
+import { SigninService } from './SigninService.js';
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+@Injectable()
+export class SigninWithPasskeyApiService {
+ private logger: Logger;
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.signinsRepository)
+ private signinsRepository: SigninsRepository,
+
+ private idService: IdService,
+ private rateLimiterService: RateLimiterService,
+ private signinService: SigninService,
+ private webAuthnService: WebAuthnService,
+ private loggerService: LoggerService,
+ ) {
+ this.logger = this.loggerService.getLogger('PasskeyAuth');
+ }
+
+ @bindThis
+ public async signin(
+ request: FastifyRequest<{
+ Body: {
+ credential?: AuthenticationResponseJSON;
+ context?: string;
+ };
+ }>,
+ reply: FastifyReply,
+ ) {
+ reply.header('Access-Control-Allow-Origin', this.config.url);
+ reply.header('Access-Control-Allow-Credentials', 'true');
+
+ const body = request.body;
+ const credential = body['credential'];
+
+ function error(status: number, error: { id: string }) {
+ reply.code(status);
+ return { error };
+ }
+
+ const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => {
+ // Append signin history
+ await this.signinsRepository.insert({
+ id: this.idService.gen(),
+ userId: userId,
+ ip: request.ip,
+ headers: request.headers as any,
+ success: false,
+ });
+ return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
+ };
+
+ try {
+ // Not more than 1 API call per 250ms and not more than 100 attempts per 30min
+ // NOTE: 1 Sign-in require 2 API calls
+ await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+ } catch (err) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
+
+ // Initiate Passkey Auth challenge with context
+ if (!credential) {
+ const context = randomUUID();
+ this.logger.info(`Initiate Passkey challenge: context: ${context}`);
+ const authChallengeOptions = {
+ option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context),
+ context: context,
+ };
+ reply.code(200);
+ return authChallengeOptions;
+ }
+
+ const context = body.context;
+ if (!context || typeof context !== 'string') {
+ // If try Authentication without context
+ return error(400, {
+ id: '1658cc2e-4495-461f-aee4-d403cdf073c1',
+ });
+ }
+
+ this.logger.debug(`Try Sign-in with Passkey: context: ${context}`);
+
+ let authorizedUserId: MiUser['id'] | null;
+ try {
+ authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
+ } catch (err) {
+ this.logger.warn(`Passkey challenge Verify error! : ${err}`);
+ const errorId = (err as IdentifiableError).id;
+ return error(403, {
+ id: errorId,
+ });
+ }
+
+ if (!authorizedUserId) {
+ return error(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ }
+
+ // Fetch user
+ const user = await this.usersRepository.findOneBy({
+ id: authorizedUserId,
+ host: IsNull(),
+ }) as MiLocalUser | null;
+
+ if (user == null) {
+ return error(403, {
+ id: '652f899f-66d4-490e-993e-6606c8ec04c3',
+ });
+ }
+
+ if (user.isSuspended) {
+ return error(403, {
+ id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
+ });
+ }
+
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+
+ // Authentication was successful, but passwordless login is not enabled
+ if (!profile.usePasswordLessLogin) {
+ return await fail(user.id, 403, {
+ id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912',
+ });
+ }
+
+ const signinResponse = this.signinService.signin(request, reply, user);
+ return {
+ signinResponse: signinResponse,
+ };
+ }
+}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index f89c3954f8..db860d710a 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -8,9 +8,8 @@ import { Inject, Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js';
+import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { IdService } from '@/core/IdService.js';
import { SignupService } from '@/core/SignupService.js';
@@ -31,6 +30,9 @@ export class SignupApiService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -48,7 +50,6 @@ export class SignupApiService {
private userEntityService: UserEntityService,
private idService: IdService,
- private metaService: MetaService,
private captchaService: CaptchaService,
private signupService: SignupService,
private signinService: SigninService,
@@ -71,37 +72,42 @@ export class SignupApiService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
+ 'frc-captcha-solution'?: string;
}
}>,
reply: FastifyReply,
) {
const body = request.body;
- const instance = await this.metaService.fetch(true);
-
// Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') {
- if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
- await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+ if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+ await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
- await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+ if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+ await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
- await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+ if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+ await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableTurnstile && instance.turnstileSecretKey) {
- await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+ await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableFC && this.meta.fcSecretKey) {
+ await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
@@ -114,7 +120,7 @@ export class SignupApiService {
const reason = body['reason'];
const emailAddress = body['emailAddress'];
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400);
return;
@@ -127,7 +133,7 @@ export class SignupApiService {
}
}
- if (instance.approvalRequiredForSignup) {
+ if (this.meta.approvalRequiredForSignup) {
if (reason == null || typeof reason !== 'string') {
reply.code(400);
return;
@@ -136,7 +142,7 @@ export class SignupApiService {
let ticket: MiRegistrationTicket | null = null;
- if (instance.disableRegistration) {
+ if (this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
@@ -157,7 +163,7 @@ export class SignupApiService {
}
// メアド認証が有効の場合
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー
if (ticket.usedBy) {
reply.code(400);
@@ -175,7 +181,7 @@ export class SignupApiService {
}
}
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
@@ -185,7 +191,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'USED_USERNAME');
}
- const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+ const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new FastifyReplyError(400, 'DENIED_USERNAME');
}
@@ -220,7 +226,7 @@ export class SignupApiService {
reply.code(204);
return;
- } else if (instance.approvalRequiredForSignup) {
+ } else if (this.meta.approvalRequiredForSignup) {
const { account } = await this.signupService.signup({
username, password, host, reason,
});
@@ -288,8 +294,6 @@ export class SignupApiService {
const code = body['code'];
- const instance = await this.metaService.fetch(true);
-
try {
const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code });
@@ -324,7 +328,7 @@ export class SignupApiService {
});
}
- if (instance.approvalRequiredForSignup) {
+ if (this.meta.approvalRequiredForSignup) {
if (pendingUser.email) {
this.emailService.sendEmail(pendingUser.email, 'Approval pending',
'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.',
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index e2fcd1a9d0..14e002929a 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -85,6 +85,7 @@ import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
+import * as ep___admin_declineUser from './endpoints/admin/decline-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
@@ -103,6 +104,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.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';
@@ -195,6 +197,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
+import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@@ -272,6 +275,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -296,6 +300,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
+import * as ep___notes_following from './endpoints/notes/following.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
@@ -303,6 +308,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
+import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js';
import * as ep___notes_reactions from './endpoints/notes/reactions.js';
import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
@@ -479,6 +485,7 @@ const eps = [
['admin/unsilence-user', ep___admin_unsilenceUser],
['admin/suspend-user', ep___admin_suspendUser],
['admin/approve-user', ep___admin_approveUser],
+ ['admin/decline-user', ep___admin_declineUser],
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/delete-account', ep___admin_deleteAccount],
@@ -497,6 +504,7 @@ const eps = [
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
+ ['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -589,6 +597,7 @@ const eps = [
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],
['following/requests/list', ep___following_requests_list],
+ ['following/requests/sent', ep___following_requests_sent],
['following/requests/reject', ep___following_requests_reject],
['gallery/featured', ep___gallery_featured],
['gallery/popular', ep___gallery_popular],
@@ -666,6 +675,7 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
+ ['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
@@ -690,6 +700,7 @@ const eps = [
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],
+ ['notes/following', ep___notes_following],
['notes/global-timeline', ep___notes_globalTimeline],
['notes/bubble-timeline', ep___notes_bubbleTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
@@ -697,6 +708,7 @@ const eps = [
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],
['notes/polls/vote', ep___notes_polls_vote],
+ ['notes/polls/refresh', ep___notes_polls_refresh],
['notes/reactions', ep___notes_reactions],
['notes/reactions/create', ep___notes_reactions_create],
['notes/reactions/delete', ep___notes_reactions_delete],
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index a7e8a3b018..7754899b95 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository } from '@/models/_.js';
+import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
-import { DI } from '@/di-symbols.js';
import { Packed } from '@/misc/json-schema.js';
+import { RoleService } from '@/core/RoleService.js';
+import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
@@ -28,6 +28,35 @@ export const meta = {
},
},
},
+
+ errors: {
+ // From ApiCallService.ts
+ noCredential: {
+ message: 'Credential required.',
+ code: 'CREDENTIAL_REQUIRED',
+ id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+ httpStatusCode: 401,
+ },
+ noAdmin: {
+ message: 'You are not assigned to an administrator role.',
+ code: 'ROLE_PERMISSION_DENIED',
+ kind: 'permission',
+ id: 'c3d38592-54c0-429d-be96-5636b0431a61',
+ },
+ noPermission: {
+ message: 'Your app does not have the necessary permissions to use this endpoint.',
+ code: 'PERMISSION_DENIED',
+ kind: 'permission',
+ id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
+ },
+ },
+
+ // Required token permissions, but we need to check them manually.
+ // ApiCallService checks access in a way that would prevent creating the first account.
+ softPermissions: [
+ 'write:admin:account',
+ 'write:admin:approve-user',
+ ],
} as const;
export const paramDef = {
@@ -42,22 +71,19 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
+ private roleService: RoleService,
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, _me, token) => {
- const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
- const realUsers = await this.instanceActorService.realLocalUsersPresent();
- if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
+ await this.ensurePermissions(_me, token);
const { account, secret } = await this.signupService.signup({
username: ps.username,
password: ps.password,
ignorePreservedUsernames: true,
+ approved: true,
});
const res = await this.userEntityService.pack(account, account, {
@@ -70,4 +96,21 @@ export default class extends Endpoint { // eslint-
return res;
});
}
+
+ private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise {
+ // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
+ if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
+ throw new ApiError(meta.errors.noPermission);
+ }
+
+ // Only administrators (including root) can create users.
+ if (me && !await this.roleService.isAdministrator(me)) {
+ throw new ApiError(meta.errors.noAdmin);
+ }
+
+ // Anonymous access is only allowed for initial instance setup.
+ if (!me && await this.instanceActorService.realLocalUsersPresent()) {
+ throw new ApiError(meta.errors.noCredential);
+ }
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/admin/decline-user.ts b/packages/backend/src/server/api/endpoints/admin/decline-user.ts
new file mode 100644
index 0000000000..0a75dd977d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/decline-user.ts
@@ -0,0 +1,75 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsedUsernamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { DI } from '@/di-symbols.js';
+import { EmailService } from '@/core/EmailService.js';
+import { DeleteAccountService } from '@/core/DeleteAccountService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:decline-user',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.usedUsernamesRepository)
+ private usedUsernamesRepository: UsedUsernamesRepository,
+
+ private moderationLogService: ModerationLogService,
+ private emailService: EmailService,
+ private deleteAccountService: DeleteAccountService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.usersRepository.findOneBy({ id: ps.userId });
+
+ if (user == null || user.isDeleted) {
+ throw new Error('user not found or already deleted');
+ }
+
+ if (user.approved) {
+ throw new Error('user is already approved');
+ }
+
+ if (user.host) {
+ throw new Error('user is not local');
+ }
+
+ const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
+
+ if (profile?.email) {
+ this.emailService.sendEmail(profile.email, 'Account Declined',
+ 'Your Account has been declined!',
+ 'Your Account has been declined!');
+ }
+
+ await this.usedUsernamesRepository.delete({ username: user.username });
+
+ await this.deleteAccountService.deleteAccount(user);
+
+ this.moderationLogService.log(me, 'decline', {
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 9e93310746..601c898f52 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -31,15 +31,20 @@ export default class extends Endpoint { // eslint-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.notesRepository)
+ @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
- const followings = await this.followingsRepository.findBy({
- followerHost: ps.host,
- });
+ const followings = await this.followingsRepository.findBy([
+ {
+ followeeHost: ps.host,
+ },
+ {
+ followerHost: ps.host,
+ },
+ ]);
const pairs = await Promise.all(followings.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }),
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index 8b142910a6..daf19c4435 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -25,6 +25,7 @@ export const paramDef = {
host: { type: 'string' },
isSuspended: { type: 'boolean' },
isNSFW: { type: 'boolean' },
+ rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' },
},
required: ['host'],
@@ -57,6 +58,7 @@ export default class extends Endpoint { // eslint-
await this.federatedInstanceService.update(instance.id, {
suspensionState,
isNSFW: ps.isNSFW,
+ rejectReports: ps.rejectReports,
moderationNote: ps.moderationNote,
});
@@ -74,6 +76,22 @@ export default class extends Endpoint { // eslint-
}
}
+ if (ps.isNSFW != null && instance.isNSFW !== ps.isNSFW) {
+ const message = ps.rejectReports ? 'setRemoteInstanceNSFW' : 'unsetRemoteInstanceNSFW';
+ this.moderationLogService.log(me, message, {
+ id: instance.id,
+ host: instance.host,
+ });
+ }
+
+ if (ps.rejectReports != null && instance.rejectReports !== ps.rejectReports) {
+ const message = ps.rejectReports ? 'rejectRemoteInstanceReports' : 'acceptRemoteInstanceReports';
+ this.moderationLogService.log(me, message, {
+ id: instance.id,
+ host: instance.host,
+ });
+ }
+
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 063bb6751b..6e368eff43 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -73,6 +73,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableFC: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ fcSiteKey: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -110,6 +118,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ sidebarLogoUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
@@ -124,7 +136,7 @@ export const meta = {
},
silencedHosts: {
type: 'array',
- optional: true,
+ optional: false,
nullable: false,
items: {
type: 'string',
@@ -215,6 +227,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ fcSecretKey: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
@@ -396,6 +412,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
+ enableReactionsBuffering: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
@@ -522,6 +542,26 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ trustedLinkUrlPatterns: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ federation: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ federationHosts: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
},
},
} as const;
@@ -572,6 +612,8 @@ export default class extends Endpoint { // eslint-
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
+ enableFC: instance.enableFC,
+ fcSiteKey: instance.fcSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -582,6 +624,7 @@ export default class extends Endpoint { // eslint-
iconUrl: instance.iconUrl,
app192IconUrl: instance.app192IconUrl,
app512IconUrl: instance.app512IconUrl,
+ sidebarLogoUrl: instance.sidebarLogoUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
defaultLightTheme: instance.defaultLightTheme,
@@ -605,6 +648,7 @@ export default class extends Endpoint { // eslint-
mcaptchaSecretKey: instance.mcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey,
+ fcSecretKey: instance.fcSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
@@ -656,6 +700,7 @@ export default class extends Endpoint { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
+ enableReactionsBuffering: instance.enableReactionsBuffering,
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
@@ -664,6 +709,9 @@ export default class extends Endpoint { // eslint-
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
urlPreviewUserAgent: instance.urlPreviewUserAgent,
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
+ trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
+ federation: instance.federation,
+ federationHosts: instance.federationHosts,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index 7a3410ffa7..f3e440b4cb 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 305ae1af1d..e7589cba81 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
index 828dbae712..e4bb545f5d 100644
--- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -11,6 +11,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { isSystemAccount } from '@/misc/is-system-account.js';
export const meta = {
tags: ['admin'],
@@ -63,6 +64,10 @@ export default class extends Endpoint { // eslint-
throw new Error('cannot reset password of root');
}
+ if (isSystemAccount(user)) {
+ throw new Error('cannot reset password of system account');
+ }
+
const passwd = secureRndstr(8);
// Generate hash of password
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index a7ca7f9547..669bffe2dc 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
import { IdService } from '@/core/IdService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
+import { isSystemAccount } from '@/misc/is-system-account.js';
export const meta = {
tags: ['admin'],
@@ -31,6 +32,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ approved: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ followedMessage: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
@@ -111,6 +120,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isSystem: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
isSilenced: {
type: 'boolean',
optional: false, nullable: false,
@@ -228,6 +241,7 @@ export default class extends Endpoint { // eslint-
emailVerified: profile.emailVerified,
approved: user.approved,
signupReason: user.signupReason,
+ followedMessage: profile.followedMessage,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,
@@ -240,6 +254,7 @@ export default class extends Endpoint { // eslint-
mutedInstances: profile.mutedInstances,
notificationRecieveConfig: profile.notificationRecieveConfig,
isModerator: isModerator,
+ isSystem: isSystemAccount(user),
isSilenced: isSilenced,
isSuspended: user.isSuspended,
isHibernated: user.isHibernated,
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
new file mode 100644
index 0000000000..fb2ddf4b44
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'read:admin:system-webhook',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string', nullable: false },
+ secret: { type: 'string', nullable: false },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ try {
+ await this.webhookTestService.testSystemWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ });
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 6bda1ae6ad..98760bbcc3 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -55,6 +55,7 @@ export const paramDef = {
iconUrl: { type: 'string', nullable: true },
app192IconUrl: { type: 'string', nullable: true },
app512IconUrl: { type: 'string', nullable: true },
+ sidebarLogoUrl: { type: 'string', nullable: true },
backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true },
name: { type: 'string', nullable: true },
@@ -80,6 +81,9 @@ export const paramDef = {
enableTurnstile: { type: 'boolean' },
turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true },
+ enableFC: { type: 'boolean' },
+ fcSiteKey: { type: 'string', nullable: true },
+ fcSecretKey: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -150,6 +154,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
+ enableReactionsBuffering: { type: 'boolean' },
notesPerOneAd: { type: 'integer' },
silencedHosts: {
type: 'array',
@@ -175,6 +180,21 @@ export const paramDef = {
urlPreviewRequireContentLength: { type: 'boolean' },
urlPreviewUserAgent: { type: 'string', nullable: true },
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
+ trustedLinkUrlPatterns: {
+ type: 'array', nullable: true, items: {
+ type: 'string',
+ },
+ },
+ federation: {
+ type: 'string',
+ enum: ['all', 'none', 'specified'],
+ },
+ federationHosts: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
},
required: [],
} as const;
@@ -250,6 +270,10 @@ export default class extends Endpoint { // eslint-
set.app512IconUrl = ps.app512IconUrl;
}
+ if (ps.sidebarLogoUrl !== undefined) {
+ set.sidebarLogoUrl = ps.sidebarLogoUrl;
+ }
+
if (ps.serverErrorImageUrl !== undefined) {
set.serverErrorImageUrl = ps.serverErrorImageUrl;
}
@@ -362,6 +386,18 @@ export default class extends Endpoint { // eslint-
set.turnstileSecretKey = ps.turnstileSecretKey;
}
+ if (ps.enableFC !== undefined) {
+ set.enableFC = ps.enableFC;
+ }
+
+ if (ps.fcSiteKey !== undefined) {
+ set.fcSiteKey = ps.fcSiteKey;
+ }
+
+ if (ps.fcSecretKey !== undefined) {
+ set.fcSecretKey = ps.fcSecretKey;
+ }
+
if (ps.enableBotTrending !== undefined) {
set.enableBotTrending = ps.enableBotTrending;
}
@@ -626,6 +662,10 @@ export default class extends Endpoint { // eslint-
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
}
+ if (ps.enableReactionsBuffering !== undefined) {
+ set.enableReactionsBuffering = ps.enableReactionsBuffering;
+ }
+
if (ps.notesPerOneAd !== undefined) {
set.notesPerOneAd = ps.notesPerOneAd;
}
@@ -660,6 +700,18 @@ export default class extends Endpoint { // eslint-
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
}
+ if (Array.isArray(ps.trustedLinkUrlPatterns)) {
+ set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean);
+ }
+
+ if (ps.federation !== undefined) {
+ set.federation = ps.federation;
+ }
+
+ if (Array.isArray(ps.federationHosts)) {
+ set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 577b9e1b1f..e0c8ddcc84 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -34,6 +34,12 @@ export const meta = {
code: 'TOO_MANY_ANTENNAS',
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
+ },
},
res: {
@@ -87,7 +93,7 @@ export default class extends Endpoint { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 0c30bca9e0..10f26b1912 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -32,6 +32,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
+ },
},
res: {
@@ -85,7 +91,7 @@ export default class extends Endpoint { // eslint-
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
}
// Fetch the antenna
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index ca6789a464..a877d1ce0d 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
@@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -91,7 +90,6 @@ export default class extends Endpoint { // eslint-
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
- private metaService: MetaService,
private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
@@ -112,10 +110,7 @@ export default class extends Endpoint { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> {
- // ブロックしてたら中断
- const host = this.utilityService.extractDbHost(uri);
- const fetchedMeta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@@ -123,6 +118,8 @@ export default class extends Endpoint { // eslint-
]));
if (local != null) return local;
+ const host = this.utilityService.extractDbHost(uri);
+
// local object, not found in db? fail
if (this.utilityService.isSelfHost(host)) return null;
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 9369481649..06130464a9 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -5,14 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
+import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
@@ -65,6 +63,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -75,16 +76,12 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
- private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
- const serverSettings = await this.metaService.fetch();
-
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
@@ -95,7 +92,7 @@ export default class extends Endpoint { // eslint-
if (me) this.activeUsersChart.read(me);
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
}
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 7e9b0fa0e1..eb45e29f9e 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { RoleService } from '@/core/RoleService.js';
@@ -41,14 +40,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- private metaService: MetaService,
private driveFileEntityService: DriveFileEntityService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
- const instance = await this.metaService.fetch(true);
-
- // Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
const policies = await this.roleService.getUserPolicies(me.id);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
index 4670392025..b86059b5e7 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -10,6 +10,7 @@ import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['drive', 'notes'],
@@ -61,12 +62,13 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch file
const file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
- userId: me.id,
+ userId: await this.roleService.isModerator(me) ? undefined : me.id,
});
if (file == null) {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 9c17f93ab2..f67ff6ddc4 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -4,14 +4,15 @@
*/
import ms from 'ms';
-import { Injectable } from '@nestjs/common';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
+import { Inject, Injectable } from '@nestjs/common';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
import { DriveService } from '@/core/DriveService.js';
+import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['drive'],
@@ -55,6 +56,12 @@ export const meta = {
code: 'NO_FREE_SPACE',
id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
},
+
+ commentTooLong: {
+ message: 'Cannot upload the file because the comment exceeds the instance limit.',
+ code: 'COMMENT_TOO_LONG',
+ id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
+ },
},
} as const;
@@ -63,7 +70,7 @@ export const paramDef = {
properties: {
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
name: { type: 'string', nullable: true, default: null },
- comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null },
+ comment: { type: 'string', nullable: true, default: null },
isSensitive: { type: 'boolean', default: false },
force: { type: 'boolean', default: false },
},
@@ -73,8 +80,13 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
+ @Inject(DI.config)
+ private config: Config,
+
private driveFileEntityService: DriveFileEntityService,
- private metaService: MetaService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
@@ -91,7 +103,9 @@ export default class extends Endpoint { // eslint-
}
}
- const instance = await this.metaService.fetch();
+ if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
+ throw new ApiError(meta.errors.commentTooLong);
+ }
try {
// Create file
@@ -103,8 +117,8 @@ export default class extends Endpoint { // eslint-
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
- requestIp: instance.enableIpLogging ? ip : null,
- requestHeaders: instance.enableIpLogging ? headers : null,
+ requestIp: this.serverSettings.enableIpLogging ? ip : null,
+ requestHeaders: this.serverSettings.enableIpLogging ? headers : null,
});
return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 5541018126..1501abf9ce 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -9,8 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
+import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
export const meta = {
tags: ['drive'],
@@ -51,6 +51,12 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE',
id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
},
+
+ commentTooLong: {
+ message: 'Cannot upload the file because the comment exceeds the instance limit.',
+ code: 'COMMENT_TOO_LONG',
+ id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
+ },
},
res: {
type: 'object',
@@ -66,7 +72,7 @@ export const paramDef = {
folderId: { type: 'string', format: 'misskey:id', nullable: true },
name: { type: 'string' },
isSensitive: { type: 'boolean' },
- comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH },
+ comment: { type: 'string', nullable: true },
},
required: ['fileId'],
} as const;
@@ -76,6 +82,8 @@ export default class extends Endpoint { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
+ @Inject(DI.config)
+ private config: Config,
private driveService: DriveService,
private roleService: RoleService,
@@ -90,6 +98,10 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
+ if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
+ throw new ApiError(meta.errors.commentTooLong);
+ }
+
let packedFile;
try {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index 49d2e78d08..e20d482e70 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -4,12 +4,14 @@
*/
import ms from 'ms';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DriveService } from '@/core/DriveService.js';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
+import { ApiError } from '@/server/api/error.js';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
export const meta = {
tags: ['drive'],
@@ -26,6 +28,14 @@ export const meta = {
prohibitMoved: true,
kind: 'write:drive',
+
+ errors: {
+ commentTooLong: {
+ message: 'Cannot upload the file because the comment exceeds the instance limit.',
+ code: 'COMMENT_TOO_LONG',
+ id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
+ },
+ },
} as const;
export const paramDef = {
@@ -34,7 +44,7 @@ export const paramDef = {
url: { type: 'string' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
isSensitive: { type: 'boolean', default: false },
- comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null },
+ comment: { type: 'string', nullable: true, default: null },
marker: { type: 'string', nullable: true, default: null },
force: { type: 'boolean', default: false },
},
@@ -44,11 +54,18 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
private driveFileEntityService: DriveFileEntityService,
private driveService: DriveService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
+ if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
+ throw new ApiError(meta.errors.commentTooLong);
+ }
+
this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', {
diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts
index ce4dd13067..d5b80035df 100644
--- a/packages/backend/src/server/api/endpoints/federation/followers.ts
+++ b/packages/backend/src/server/api/endpoints/federation/followers.ts
@@ -3,17 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { FollowingsRepository } from '@/models/_.js';
-import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
-import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['federation'],
- requireCredential: false,
+ requireCredential: true,
+ kind: 'read:account',
res: {
type: 'array',
@@ -33,6 +31,8 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ includeFollower: { type: 'boolean', default: false },
+ includeFollowee: { type: 'boolean', default: true },
},
required: ['host'],
} as const;
@@ -40,21 +40,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
private followingEntityService: FollowingEntityService,
- private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId)
- .andWhere('following.followeeHost = :host', { host: ps.host });
-
- const followings = await query
- .limit(ps.limit)
- .getMany();
-
- return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
+ return this.followingEntityService.getFollowers(me, ps);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts
index 1a793889c7..215f94fbcc 100644
--- a/packages/backend/src/server/api/endpoints/federation/following.ts
+++ b/packages/backend/src/server/api/endpoints/federation/following.ts
@@ -3,17 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { FollowingsRepository } from '@/models/_.js';
-import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
-import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['federation'],
- requireCredential: false,
+ requireCredential: true,
+ kind: 'read:account',
res: {
type: 'array',
@@ -33,6 +31,8 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ includeFollower: { type: 'boolean', default: false },
+ includeFollowee: { type: 'boolean', default: true },
},
required: ['host'],
} as const;
@@ -40,21 +40,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
private followingEntityService: FollowingEntityService,
- private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId)
- .andWhere('following.followerHost = :host', { host: ps.host });
-
- const followings = await query
- .limit(ps.limit)
- .getMany();
-
- return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
+ return this.followingEntityService.getFollowing(me, ps);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts
new file mode 100644
index 0000000000..6325f01bb8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import type { FollowRequestsRepository } from '@/models/_.js';
+import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ tags: ['following', 'account'],
+
+ requireCredential: true,
+
+ kind: 'read:following',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ follower: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ followee: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.followRequestsRepository)
+ private followRequestsRepository: FollowRequestsRepository,
+
+ private followRequestEntityService: FollowRequestEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
+ .andWhere('request.followerId = :meId', { meId: me.id });
+
+ const requests = await query
+ .limit(ps.limit)
+ .getMany();
+
+ return await this.followRequestEntityService.packMany(requests, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index 73231e8e09..603df44733 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -3,10 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta } from '@/models/_.js';
export const meta = {
requireCredential: true,
@@ -25,12 +26,13 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private achievementService: AchievementService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
- const meta = await this.metaService.fetch();
- if (!meta.enableAchievements) return;
+ if (!this.serverSettings.enableAchievements) return;
await this.achievementService.create(me.id, ps.name);
});
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index bc46163e3d..bdf6c065e8 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportAntennas',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 2606108539..d7bb6bcd22 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportBlocking',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index d5e824df27..e03192d8c6 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportFollowing',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 0f5800404e..76b285bb7e 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportMuting',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index bacdd5c88f..76ecfd082c 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportUserLists',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index 7332026d84..0be8bfb695 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -8,7 +8,7 @@ import ms from 'ms';
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/_.js';
+import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmailService } from '@/core/EmailService.js';
import type { Config } from '@/config.js';
@@ -16,7 +16,6 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -71,10 +70,12 @@ export default class extends Endpoint { // eslint-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
@@ -106,7 +107,7 @@ export default class extends Endpoint { // eslint-
if (!res.available) {
throw new ApiError(meta.errors.unavailable);
}
- } else if ((await this.metaService.fetch()).emailRequiredForSignup) {
+ } else if (this.serverSettings.emailRequiredForSignup) {
throw new ApiError(meta.errors.emailRequired);
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 6cc22e7994..8994c3fff6 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
+import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
-import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -146,6 +145,7 @@ export const paramDef = {
properties: {
name: { ...nameSchema, nullable: true },
description: { ...descriptionSchema, nullable: true },
+ followedMessage: { ...followedMessageSchema, nullable: true },
location: { ...locationSchema, nullable: true },
birthday: { ...birthdaySchema, nullable: true },
listenbrainz: { ...listenbrainzSchema, nullable: true },
@@ -159,6 +159,7 @@ export const paramDef = {
flipH: { type: 'boolean', nullable: true },
offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 },
offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 },
+ showBelow: { type: 'boolean', nullable: true },
},
required: ['id'],
} },
@@ -192,6 +193,7 @@ export const paramDef = {
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
+ defaultSensitive: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
@@ -283,6 +285,7 @@ export default class extends Endpoint { // eslint-
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description;
+ if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
@@ -347,6 +350,7 @@ export default class extends Endpoint { // eslint-
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
}
+ if (typeof ps.defaultSensitive === 'boolean') profileUpdates.defaultSensitive = ps.defaultSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) {
@@ -399,7 +403,7 @@ export default class extends Endpoint { // eslint-
updates.backgroundUrl = null;
updates.backgroundBlurhash = null;
}
-
+
if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);
@@ -417,6 +421,7 @@ export default class extends Endpoint { // eslint-
flipH: d.flipH ?? false,
offsetX: d.offsetX ?? 0,
offsetY: d.offsetY ?? 0,
+ showBelow: d.showBelow ?? false,
}));
}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index 9eb7f5b3a0..6e84603f7a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index fe07afb2d0..394c178f2a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks', 'account'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index 5ddb79caf2..4a0c09ff0c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
new file mode 100644
index 0000000000..2bf6df9ce2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ secure: true,
+ kind: 'read:account',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: webhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string' },
+ secret: { type: 'string' },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ await this.webhookTestService.testUserWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ }, me);
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts
index 2786bd98d5..2ffd41ae28 100644
--- a/packages/backend/src/server/api/endpoints/invite/limit.ts
+++ b/packages/backend/src/server/api/endpoints/invite/limit.ts
@@ -49,7 +49,7 @@ export default class extends Endpoint { // eslint-
const policies = await this.roleService.getUserPolicies(me.id);
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
- id: MoreThan(this.idService.gen(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
+ id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 60 * 1000))),
createdById: me.id,
}) : null;
diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
index c5e3a5a5f7..94ec8c37ec 100644
--- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
-import type { NotesRepository } from '@/models/_.js';
+import type { NotesRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -9,7 +9,6 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
-import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['notes'],
@@ -51,6 +50,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -59,11 +61,9 @@ export default class extends Endpoint { // eslint-
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
- const instance = await this.metaService.fetch();
if (!policies.btlAvailable) {
throw new ApiError(meta.errors.btlDisabled);
}
@@ -79,7 +79,7 @@ export default class extends Endpoint { // eslint-
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
- .andWhere('note.userHost IN (:...hosts)', { hosts: instance.bubbleInstances })
+ .andWhere('note.userHost IN (:...hosts)', { hosts: this.serverSettings.bubbleInstances })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
@@ -97,7 +97,7 @@ export default class extends Endpoint { // eslint-
}
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
-
+
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.where('note.renoteId IS NULL');
diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts
index f3d887bb20..18d80e867b 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.test.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts
@@ -5,15 +5,12 @@
process.env.NODE_ENV = 'test';
-import { readFile } from 'node:fs/promises';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
import { describe, test, expect } from '@jest/globals';
+import { loadConfig } from '@/config.js';
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
import { paramDef } from './create.js';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
+const config = loadConfig();
const VALID = true;
const INVALID = false;
@@ -21,7 +18,12 @@ const INVALID = false;
describe('api:notes/create', () => {
describe('validation', () => {
const v = getValidator(paramDef);
- const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8');
+ const tooLong = (limit: number) => {
+ const arr: string[] = [''];
+ arr.length = limit + 1;
+ arr.fill('a');
+ return arr.join('');
+ };
test('reject empty', () => {
const valid = v({ });
@@ -71,8 +73,8 @@ describe('api:notes/create', () => {
.toBe(INVALID);
});
- test('over 500 characters cw', async () => {
- expect(v({ text: 'Body', cw: await tooLong }))
+ test('over max characters cw', async () => {
+ expect(v({ text: '', cw: tooLong(config.maxNoteLength) }))
.toBe(INVALID);
});
});
@@ -220,7 +222,7 @@ describe('api:notes/create', () => {
});
test('reject poll with too long choice', async () => {
- expect(v({ poll: { choices: [await tooLong, '2'] } }))
+ expect(v({ poll: { choices: [tooLong(config.maxNoteLength), '2'] } }))
.toBe(INVALID);
});
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 626f03b758..d1cf0123dc 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
-import { MetaService } from '@/core/MetaService.js';
-import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
@@ -92,6 +90,12 @@ export const meta = {
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
},
+ maxCwLength: {
+ message: 'You tried posting a content warning which is too long.',
+ code: 'MAX_CW_LENGTH',
+ id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
+ },
+
cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
message: 'You cannot reply to a specified visibility note with extended visibility.',
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
@@ -149,7 +153,7 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
- cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
+ cw: { type: 'string', nullable: true, minLength: 1 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
@@ -252,9 +256,12 @@ export default class extends Endpoint { // eslint-
private noteCreateService: NoteCreateService,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.text && (ps.text.length > this.config.maxNoteLength)) {
+ if (ps.text && ps.text.length > this.config.maxNoteLength) {
throw new ApiError(meta.errors.maxLength);
}
+ if (ps.cw && ps.cw.length > this.config.maxCwLength) {
+ throw new ApiError(meta.errors.maxCwLength);
+ }
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index 835cbc14fa..dc94c78e75 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -86,6 +86,12 @@ export const meta = {
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
},
+ maxCwLength: {
+ message: 'You tried posting a content warning which is too long.',
+ code: 'MAX_CW_LENGTH',
+ id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
+ },
+
cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
message: 'You cannot reply to a specified visibility note with extended visibility.',
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
@@ -197,7 +203,7 @@ export const paramDef = {
format: 'misskey:id',
},
},
- cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
+ cw: { type: 'string', nullable: true, minLength: 1 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
@@ -297,9 +303,13 @@ export default class extends Endpoint { // eslint-
private noteEditService: NoteEditService,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.text && (ps.text.length > this.config.maxNoteLength)) {
+ if (ps.text && ps.text.length > this.config.maxNoteLength) {
throw new ApiError(meta.errors.maxLength);
}
+ if (ps.cw && ps.cw.length > this.config.maxCwLength) {
+ throw new ApiError(meta.errors.maxCwLength);
+ }
+
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({
diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts
new file mode 100644
index 0000000000..b6604b9798
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/following.ts
@@ -0,0 +1,178 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
+import { SkLatestNote, MiFollowing } from '@/models/_.js';
+import type { NotesRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { QueryService } from '@/core/QueryService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ kind: 'read:account',
+ allowGet: true,
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Note',
+ },
+ },
+
+ errors: {
+ bothWithRepliesAndWithFiles: {
+ message: 'Specifying both includeReplies and filesOnly is not supported',
+ code: 'BOTH_INCLUDE_REPLIES_AND_FILES_ONLY',
+ id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
+ },
+ bothWithFollowersAndIncludeNonPublic: {
+ message: 'Specifying both list:followers and includeNonPublic is not supported',
+ code: 'BOTH_LIST_FOLLOWERS_AND_INCLUDE_NON_PUBLIC',
+ id: '7a1b9cb6-235b-4e58-9c00-32c1796f502c',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ list: { type: 'string', enum: ['following', 'followers', 'mutuals'], default: 'following' },
+
+ filesOnly: { type: 'boolean', default: false },
+ includeNonPublic: { type: 'boolean', default: false },
+ includeReplies: { type: 'boolean', default: false },
+ includeQuotes: { type: 'boolean', default: false },
+ includeBots: { type: 'boolean', default: true },
+
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ sinceDate: { type: 'integer' },
+ untilDate: { type: 'integer' },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ if (ps.includeReplies && ps.filesOnly) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
+ if (ps.list === 'followers' && ps.includeNonPublic) throw new ApiError(meta.errors.bothWithFollowersAndIncludeNonPublic);
+
+ const query = this.notesRepository
+ .createQueryBuilder('note')
+ .setParameter('me', me.id)
+
+ // Limit to latest notes
+ .innerJoin(
+ (sub: SelectQueryBuilder) => {
+ sub
+ .from(SkLatestNote, 'latest')
+
+ // Return only one note per user
+ .addSelect('latest.user_id', 'user_id')
+ .addSelect('MAX(latest.note_id)', 'note_id')
+ .groupBy('latest.user_id');
+
+ // Match selected note types.
+ if (!ps.includeNonPublic) {
+ sub.andWhere('latest.is_public = true');
+ }
+ if (!ps.includeReplies) {
+ sub.andWhere('latest.is_reply = false');
+ }
+ if (!ps.includeQuotes) {
+ sub.andWhere('latest.is_quote = false');
+ }
+
+ return sub;
+ },
+ 'latest',
+ 'note.id = latest.note_id',
+ )
+
+ // Avoid N+1 queries from the "pack" method
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('note.channel', 'channel')
+ ;
+
+ // Select the appropriate collection of users
+ if (ps.list === 'followers') {
+ addFollower(query);
+ } else if (ps.list === 'following') {
+ addFollowee(query);
+ } else {
+ addMutual(query);
+ }
+
+ // Limit to files, if requested
+ if (ps.filesOnly) {
+ query.andWhere('note."fileIds" != \'{}\'');
+ }
+
+ // Match selected user types.
+ if (!ps.includeBots) {
+ query.andWhere('"user"."isBot" = false');
+ }
+
+ // Respect blocks and mutes
+ this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserQuery(query, me);
+
+ // Support pagination
+ this.queryService
+ .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .orderBy('note.id', 'DESC')
+ .take(ps.limit);
+
+ // Query and return the next page
+ const notes = await query.getMany();
+ return await this.noteEntityService.packMany(notes, me);
+ });
+ }
+}
+
+/**
+ * Limit to followers (they follow us)
+ */
+function addFollower>(query: T): T {
+ return query.innerJoin(MiFollowing, 'follower', 'follower."followerId" = latest.user_id AND follower."followeeId" = :me');
+}
+
+/**
+ * Limit to followees (we follow them)
+ */
+function addFollowee>(query: T): T {
+ return query.innerJoin(MiFollowing, 'followee', 'followee."followerId" = :me AND followee."followeeId" = latest.user_id');
+}
+
+/**
+ * Limit to mutuals (they follow us AND we follow them)
+ */
+function addMutual>(query: T): T {
+ addFollower(query);
+ addFollowee(query);
+ return query;
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index fdc9a77956..75be7b9888 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -75,6 +74,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -88,7 +90,6 @@ export default class extends Endpoint { // eslint-
private cacheService: CacheService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
- private metaService: MetaService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -102,9 +103,7 @@ export default class extends Endpoint { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -158,7 +157,7 @@ export default class extends Endpoint { // eslint-
allowPartial: ps.allowPartial,
me,
redisTimelines: timelineConfig,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
excludeBots: !ps.withBots,
diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts
index 17ee937360..593463aea0 100644
--- a/packages/backend/src/server/api/endpoints/notes/like.ts
+++ b/packages/backend/src/server/api/endpoints/notes/like.ts
@@ -1,8 +1,9 @@
-import { Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ReactionService } from '@/core/ReactionService.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta } from '@/models/_.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -26,6 +27,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
},
+
+ cannotReactToRenote: {
+ message: 'You cannot like a Renote.',
+ code: 'CANNOT_REACT_TO_RENOTE',
+ id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
+ },
},
} as const;
@@ -41,13 +48,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private getterService: GetterService,
private reactionService: ReactionService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
- const instance = await this.metaService.fetch();
- const like = ps.override ?? instance.defaultLike;
+ const like = ps.override ?? this.serverSettings.defaultLike;
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
@@ -58,6 +66,7 @@ export default class extends Endpoint { // eslint-
return;
}
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
+ if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
throw err;
});
return;
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index 5c3c7ae7d0..d4c806d7e2 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -5,16 +5,14 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -67,6 +65,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -74,10 +75,8 @@ export default class extends Endpoint { // eslint-
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
- private cacheService: CacheService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -90,9 +89,7 @@ export default class extends Endpoint { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -117,7 +114,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines:
ps.withFiles ? ['localTimelineWithFiles']
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts
new file mode 100644
index 0000000000..b96691f894
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts
@@ -0,0 +1,98 @@
+/*
+ * SPDX-FileCopyrightText: marie and sharkey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { UserBlockingService } from '@/core/UserBlockingService.js';
+import { ApQuestionService } from '@/core/activitypub/models/ApQuestionService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ kind: 'read:federation',
+
+ errors: {
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396',
+ },
+
+ noPoll: {
+ message: 'The note does not attach a poll.',
+ code: 'NO_POLL',
+ id: '5f979967-52d9-4314-a911-1c673727f92f',
+ },
+
+ noUri: {
+ message: 'The note has no URI defined.',
+ code: 'INVALID_URI',
+ id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260ca',
+ },
+
+ youHaveBeenBlocked: {
+ message: 'You cannot refresh this poll because you have been blocked by this user.',
+ code: 'YOU_HAVE_BEEN_BLOCKED',
+ id: '85a5377e-b1e9-4617-b0b9-5bea73331e49',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ noteId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['noteId'],
+} as const;
+
+// TODO: ロジックをサービスに切り出す
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private getterService: GetterService,
+ private userBlockingService: UserBlockingService,
+ private apQuestionService: ApQuestionService,
+ private noteEntityService: NoteEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Get note
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ if (!note.hasPoll) {
+ throw new ApiError(meta.errors.noPoll);
+ }
+
+ // Check blocking
+ if (note.userId !== me.id) {
+ const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
+ if (blocked) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
+
+ if (!note.uri) {
+ throw new ApiError(meta.errors.noUri);
+ }
+
+ await this.apQuestionService.updateQuestion(note.uri);
+
+ return await this.noteEntityService.pack(note, me, {
+ detail: true,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 55ff6771b1..2b4885a194 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -5,14 +5,13 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { NotesRepository, MiMeta } from '@/models/_.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -69,18 +68,18 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
- private metaService: MetaService,
private cacheService: CacheService,
private utilityService: UtilityService,
) {
super(meta, paramDef, async (ps, me) => {
- const meta = await this.metaService.fetch(true);
-
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.visibility = \'public\'')
.innerJoinAndSelect('note.user', 'user')
@@ -89,7 +88,7 @@ export default class extends Endpoint { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- if (!meta.enableBotTrending) query.andWhere('user.isBot = FALSE');
+ if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
@@ -156,8 +155,10 @@ export default class extends Endpoint { // eslint-
notes = notes.filter(note => {
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
if (note.user?.isSuspended) return false;
- if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) return false;
- if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false;
+ if (note.userHost) {
+ if (!this.utilityService.isFederationAllowedHost(note.userHost)) return false;
+ if (this.utilityService.isSilencedHost(this.serverSettings.silencedHosts, note.userHost)) return false;
+ }
return true;
});
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 1a14703e6e..d40a04c1b1 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
export const meta = {
@@ -57,6 +56,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -70,15 +72,12 @@ export default class extends Endpoint { // eslint-
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private userFollowingService: UserFollowingService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -110,7 +109,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index d6ef655291..234248db5c 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -4,14 +4,15 @@
*/
import { URLSearchParams } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -59,9 +60,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private noteEntityService: NoteEntityService,
private getterService: GetterService,
- private metaService: MetaService,
private httpRequestService: HttpRequestService,
private roleService: RoleService,
) {
@@ -84,13 +87,11 @@ export default class extends Endpoint { // eslint-
return;
}
- const instance = await this.metaService.fetch();
-
- if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
+ if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) {
throw new ApiError(meta.errors.unavailable);
}
- if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
+ if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) {
throw new ApiError(meta.errors.unavailable);
}
@@ -98,11 +99,11 @@ export default class extends Endpoint { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
const params = new URLSearchParams();
- if (instance.deeplAuthKey) params.append('auth_key', instance.deeplAuthKey);
+ if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
- const endpoint = instance.deeplFreeMode && instance.deeplFreeInstance ? instance.deeplFreeInstance : instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+ const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
@@ -112,7 +113,7 @@ export default class extends Endpoint { // eslint-
},
body: params.toString(),
});
- if (instance.deeplAuthKey) {
+ if (this.serverSettings.deeplAuthKey) {
const json = (await res.json()) as {
translations: {
detected_source_language: string;
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 43877e61ef..87f9b322a6 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -5,16 +5,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
-import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
-import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -69,6 +67,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -80,11 +81,9 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart,
- private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -99,9 +98,7 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.noSuchList);
}
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb(list, {
untilId,
sinceId,
@@ -115,7 +112,7 @@ export default class extends Endpoint { // eslint-
this.activeUsersChart.read(me);
- await this.noteEntityService.packMany(timeline, me);
+ return await this.noteEntityService.packMany(timeline, me);
}
const timeline = await this.fanoutTimelineEndpointService.timeline({
@@ -124,7 +121,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 15832ef7f8..5b0b656c63 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
import * as Acct from '@/misc/acct.js';
import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -38,16 +37,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const meta = await this.metaService.fetch();
-
- const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
+ const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(),
})));
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index c13802eb06..8301c85f2e 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -5,9 +5,10 @@
import * as os from 'node:os';
import si from 'systeminformation';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: false,
@@ -73,10 +74,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
) {
super(meta, paramDef, async () => {
- if (!(await this.metaService.fetch()).enableServerMachineStats) return {
+ if (!this.serverSettings.enableServerMachineStats) return {
machine: '?',
cpu: {
model: '?',
diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts
index 99414e739a..2a8a461a8f 100644
--- a/packages/backend/src/server/api/endpoints/sponsors.ts
+++ b/packages/backend/src/server/api/endpoints/sponsors.ts
@@ -3,14 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { DI } from '@/di-symbols.js';
+import { SponsorsService } from '@/core/SponsorsService.js';
export const meta = {
tags: ['meta'],
- description: 'Get Sharkey Sponsors',
+ description: 'Get Sharkey Sponsors or Instance Sponsors',
requireCredential: false,
requireCredentialPrivateMode: false,
@@ -20,6 +19,7 @@ export const paramDef = {
type: 'object',
properties: {
forceUpdate: { type: 'boolean', default: false },
+ instance: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -27,31 +27,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.redis) private redisClient: Redis.Redis,
+ private sponsorsService: SponsorsService,
) {
super(meta, paramDef, async (ps, me) => {
- let totalSponsors;
- const cachedSponsors = await this.redisClient.get('sponsors');
-
- if (!ps.forceUpdate && cachedSponsors) {
- totalSponsors = JSON.parse(cachedSponsors);
+ if (ps.instance) {
+ return { sponsor_data: await this.sponsorsService.instanceSponsors(ps.forceUpdate) };
} else {
- try {
- const backers = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json());
- const sponsorsOC = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json());
-
- // Merge both together into one array and make sure it only has Active subscriptions
- const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive === true);
-
- // Remove possible duplicates
- totalSponsors = [...new Map(allSponsors.map(v => [v.profile, v])).values()];
-
- await this.redisClient.set('sponsors', JSON.stringify(totalSponsors), 'EX', 3600);
- } catch (error) {
- totalSponsors = [];
- }
+ return { sponsor_data: await this.sponsorsService.sharkeySponsors(ps.forceUpdate) };
}
- return { sponsor_data: totalSponsors };
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index a9a33149f9..fd76df2d3c 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -5,9 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { IdService } from '@/core/IdService.js';
-import type { SwSubscriptionsRepository } from '@/models/_.js';
+import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
@@ -62,11 +61,13 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
private idService: IdService,
- private metaService: MetaService,
private pushNotificationService: PushNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -78,12 +79,10 @@ export default class extends Endpoint { // eslint-
publickey: ps.publickey,
});
- const instance = await this.metaService.fetch(true);
-
if (exist != null) {
return {
state: 'already-subscribed' as const,
- key: instance.swPublicKey,
+ key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage,
@@ -103,7 +102,7 @@ export default class extends Endpoint { // eslint-
return {
state: 'subscribed' as const,
- key: instance.swPublicKey,
+ key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: ps.endpoint,
sendReadMessage: ps.sendReadMessage,
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
index affb0996f1..4944be9b05 100644
--- a/packages/backend/src/server/api/endpoints/username/available.ts
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { localUsernameSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['users'],
@@ -39,13 +38,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
-
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const exist = await this.usersRepository.countBy({
@@ -55,8 +55,7 @@ export default class extends Endpoint { // eslint-
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
- const meta = await this.metaService.fetch();
- const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
+ const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
return {
available: exist === 0 && exist2 === 0 && !isPreserved,
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index cc76c12f1d..263d062961 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -5,18 +5,18 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
import { ApiError } from '@/server/api/error.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
export const meta = {
tags: ['users', 'notes'],
@@ -51,7 +51,11 @@ export const paramDef = {
properties: {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean', default: false },
+ withRepliesToSelf: { type: 'boolean', default: true },
+ withQuotes: { type: 'boolean', default: true },
withRenotes: { type: 'boolean', default: true },
+ withBots: { type: 'boolean', default: true },
+ withNonPublic: { type: 'boolean', default: true },
withChannelNotes: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
@@ -67,6 +71,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -75,15 +82,12 @@ export default class extends Endpoint { // eslint-
private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isSelf = me && (me.id === ps.userId);
- const serverSettings = await this.metaService.fetch();
-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
// early return if me is blocked by requesting user
@@ -94,7 +98,7 @@ export default class extends Endpoint { // eslint-
}
}
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -103,6 +107,11 @@ export default class extends Endpoint { // eslint-
withChannelNotes: ps.withChannelNotes,
withFiles: ps.withFiles,
withRenotes: ps.withRenotes,
+ withQuotes: ps.withQuotes,
+ withBots: ps.withBots,
+ withNonPublic: ps.withNonPublic,
+ withRepliesToOthers: ps.withReplies,
+ withRepliesToSelf: ps.withRepliesToSelf,
}, me);
return await this.noteEntityService.packMany(timeline, me);
@@ -127,11 +136,17 @@ export default class extends Endpoint { // eslint-
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
excludePureRenotes: !ps.withRenotes,
+ excludeBots: !ps.withBots,
noteFilter: note => {
if (note.channel?.isSensitive && !isSelf) return false;
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
+ // These are handled by DB fallback, but we duplicate them here in case a timeline was already populated with notes
+ if (!ps.withRepliesToSelf && note.reply?.userId === note.userId) return false;
+ if (!ps.withQuotes && isRenote(note) && isQuote(note)) return false;
+ if (!ps.withNonPublic && note.visibility !== 'public') return false;
+
return true;
},
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
@@ -142,6 +157,11 @@ export default class extends Endpoint { // eslint-
withChannelNotes: ps.withChannelNotes,
withFiles: ps.withFiles,
withRenotes: ps.withRenotes,
+ withQuotes: ps.withQuotes,
+ withBots: ps.withBots,
+ withNonPublic: ps.withNonPublic,
+ withRepliesToOthers: ps.withReplies,
+ withRepliesToSelf: ps.withRepliesToSelf,
}, me),
});
@@ -157,6 +177,11 @@ export default class extends Endpoint { // eslint-
withChannelNotes: boolean,
withFiles: boolean,
withRenotes: boolean,
+ withQuotes: boolean,
+ withBots: boolean,
+ withNonPublic: boolean,
+ withRepliesToOthers: boolean,
+ withRepliesToSelf: boolean,
}, me: MiLocalUser | null) {
const isSelf = me && (me.id === ps.userId);
@@ -188,7 +213,9 @@ export default class extends Endpoint { // eslint-
query.andWhere('note.fileIds != \'{}\'');
}
- if (ps.withRenotes === false) {
+ if (!ps.withRenotes && !ps.withQuotes) {
+ query.andWhere('note.renoteId IS NULL');
+ } else if (!ps.withRenotes) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :userId', { userId: ps.userId });
qb.orWhere('note.renoteId IS NULL');
@@ -196,6 +223,35 @@ export default class extends Endpoint { // eslint-
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
+ } else if (!ps.withQuotes) {
+ query.andWhere(`
+ (
+ note."renoteId" IS NULL
+ OR (
+ note.text IS NULL
+ AND note.cw IS NULL
+ AND note."replyId" IS NULL
+ AND note."hasPoll" IS FALSE
+ AND note."fileIds" = '{}'
+ )
+ )
+ `);
+ }
+
+ if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) {
+ query.andWhere('reply.id IS NULL');
+ } else if (!ps.withRepliesToOthers) {
+ query.andWhere('(reply.id IS NULL OR reply."userId" = note."userId")');
+ } else if (!ps.withRepliesToSelf) {
+ query.andWhere('(reply.id IS NULL OR reply."userId" != note."userId")');
+ }
+
+ if (!ps.withNonPublic) {
+ query.andWhere('note.visibility = \'public\'');
+ }
+
+ if (!ps.withBots) {
+ query.andWhere('"user"."isBot" = false');
}
return await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index 8c9cca1730..b40e4cdaa4 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -8,11 +8,10 @@ import megalodon, { Entity, MegalodonInterface } from 'megalodon';
import querystring from 'querystring';
import { IsNull } from 'typeorm';
import multer from 'fastify-multer';
-import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js';
import { getInstance } from './endpoints/meta.js';
import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
@@ -31,6 +30,8 @@ export function getClient(BASE_URL: string, authorization: string | undefined):
@Injectable()
export class MastodonApiServerService {
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
@@ -43,7 +44,6 @@ export class MastodonApiServerService {
private accessTokensRepository: AccessTokensRepository,
@Inject(DI.config)
private config: Config,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private driveService: DriveService,
private mastoConverter: MastoConverters,
@@ -112,7 +112,7 @@ export class MastodonApiServerService {
order: { id: 'ASC' },
});
const contact = admin == null ? null : await this.mastoConverter.convertAccount((await client.getAccount(admin.id)).data);
- reply.send(await getInstance(data.data, contact as Entity.Account, this.config, await this.metaService.fetch()));
+ reply.send(await getInstance(data.data, contact as Entity.Account, this.config, this.serverSettings));
} catch (e: any) {
/* console.error(e); */
reply.code(401).send(e.response.data);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index 79d2c62a24..c9833b85d7 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -54,7 +54,7 @@ export async function getInstance(
},
polls: {
max_options: 10,
- max_characters_per_option: 50,
+ max_characters_per_option: 150,
min_expiration: 50,
max_expiration: 2629746,
},
diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 647e9cab81..8693f0c6ac 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 226e161122..9939aa49ee 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -30,7 +30,7 @@ class ChannelChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
if (typeof params.channelId !== 'string') return;
this.channelId = params.channelId;
this.withFiles = !!(params.withFiles ?? false);
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 0af90a844b..c14f6c9123 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
-import { FastifyAdapter } from '@bull-board/fastify';
+import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify';
import ms from 'ms';
import sharp from 'sharp';
import pug from 'pug';
@@ -24,7 +24,6 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
-import { MetaService } from '@/core/MetaService.js';
import type {
DbQueue,
DeliverQueue,
@@ -61,7 +60,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
-const viteOut = `${_dirname}/../../../../../built/_vite_/`;
+const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
+const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
@Injectable()
@@ -72,6 +72,9 @@ export class ClientServerService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -108,7 +111,6 @@ export class ClientServerService {
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
- private metaService: MetaService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
private roleService: RoleService,
@@ -128,39 +130,37 @@ export class ClientServerService {
@bindThis
private async manifestHandler(reply: FastifyReply) {
- const instance = await this.metaService.fetch(true);
-
let manifest = {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'short_name': instance.shortName || instance.name || this.config.host,
+ 'short_name': this.meta.shortName || this.meta.name || this.config.host,
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'name': instance.name || this.config.host,
+ 'name': this.meta.name || this.config.host,
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'theme_color': instance.themeColor || '#86b300',
+ 'theme_color': this.meta.themeColor || '#86b300',
'icons': [{
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'src': instance.app192IconUrl || '/static-assets/icons/192.png',
+ 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'src': instance.app512IconUrl || '/static-assets/icons/512.png',
+ 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'src': instance.app512IconUrl || '/static-assets/icons/512.png',
+ 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '300x300',
'type': 'image/png',
'purpose': 'any',
@@ -179,7 +179,7 @@ export class ClientServerService {
manifest = {
...manifest,
- ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
+ ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride),
};
reply.header('Cache-Control', 'max-age=300');
@@ -193,9 +193,9 @@ export class ClientServerService {
icon: meta.iconUrl,
appleTouchIcon: meta.app512IconUrl,
themeColor: meta.themeColor,
- serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://launcher.moe/error.png',
- infoImageUrl: meta.infoImageUrl ?? 'https://launcher.moe/nothinghere.png',
- notFoundImageUrl: meta.notFoundImageUrl ?? 'https://launcher.moe/missingpage.webp',
+ serverErrorImageUrl: meta.serverErrorImageUrl ?? '/client-assets/status/error.png',
+ infoImageUrl: meta.infoImageUrl ?? '/client-assets/status/nothinghere.png',
+ notFoundImageUrl: meta.notFoundImageUrl ?? '/client-assets/status/missingpage.webp',
instanceUrl: this.config.url,
randomMOTD: this.config.customMOTD ? this.config.customMOTD[Math.floor(Math.random() * this.config.customMOTD.length)] : undefined,
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
@@ -242,7 +242,7 @@ export class ClientServerService {
}
});
- const serverAdapter = new FastifyAdapter();
+ const bullBoardServerAdapter = new BullBoardFastifyAdapter();
createBullBoard({
queues: [
@@ -255,11 +255,11 @@ export class ClientServerService {
this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
- serverAdapter,
+ serverAdapter: bullBoardServerAdapter,
});
- serverAdapter.setBasePath(bullBoardPath);
- (fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
+ bullBoardServerAdapter.setBasePath(bullBoardPath);
+ (fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion
fastify.register(fastifyView, {
@@ -280,15 +280,22 @@ export class ClientServerService {
});
//#region vite assets
- if (this.config.clientManifestExists) {
+ if (this.config.frontendEmbedManifestExists) {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
- root: viteOut,
+ root: frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
+ fastify.register(fastifyStatic, {
+ root: frontendEmbedViteOut,
+ prefix: '/embed_vite/',
+ maxAge: ms('30 days'),
+ immutable: true,
+ decorateReply: false,
+ });
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
});
@@ -299,6 +306,13 @@ export class ClientServerService {
prefix: '/vite',
rewritePrefix: '/vite',
});
+
+ const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
+ fastify.register(fastifyProxy, {
+ upstream: 'http://localhost:' + embedPort,
+ prefix: '/embed_vite',
+ rewritePrefix: '/embed_vite',
+ });
}
//#endregion
@@ -443,15 +457,20 @@ export class ClientServerService {
// Manifest
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
+ // Embed Javascript
+ fastify.get('/embed.js', async (request, reply) => {
+ return await reply.sendFile('/embed.js', staticAssets, {
+ maxAge: ms('1 day'),
+ });
+ });
+
fastify.get('/robots.txt', async (request, reply) => {
return await reply.sendFile('/robots.txt', staticAssets);
});
// OpenSearch XML
fastify.get('/opensearch.xml', async (request, reply) => {
- const meta = await this.metaService.fetch();
-
- const name = meta.name ?? 'Sharkey';
+ const name = this.meta.name ?? 'Sharkey';
let content = '';
content += '';
content += `${name} `;
@@ -468,14 +487,13 @@ export class ClientServerService {
//#endregion
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', {
- img: meta.bannerUrl,
+ img: this.meta.bannerUrl,
url: this.config.url,
- title: meta.name ?? 'Misskey',
- desc: meta.description,
- ...await this.generateCommonPugData(meta),
+ title: this.meta.name ?? 'Sharkey',
+ desc: this.meta.description,
+ ...await this.generateCommonPugData(this.meta),
...data,
});
};
@@ -553,7 +571,6 @@ export class ClientServerService {
if (user != null) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- const meta = await this.metaService.fetch();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
@@ -569,7 +586,7 @@ export class ClientServerService {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
// リモートユーザーなので
@@ -607,7 +624,6 @@ export class ClientServerService {
if (note) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -619,7 +635,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -644,7 +660,6 @@ export class ClientServerService {
if (page) {
const _page = await this.pageEntityService.pack(page);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
- const meta = await this.metaService.fetch();
if (['public'].includes(page.visibility)) {
reply.header('Cache-Control', 'public, max-age=15');
} else {
@@ -658,7 +673,7 @@ export class ClientServerService {
page: _page,
profile,
avatarUrl: _page.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -674,7 +689,6 @@ export class ClientServerService {
if (flash) {
const _flash = await this.flashEntityService.pack(flash);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -684,7 +698,7 @@ export class ClientServerService {
flash: _flash,
profile,
avatarUrl: _flash.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -700,7 +714,6 @@ export class ClientServerService {
if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -710,7 +723,7 @@ export class ClientServerService {
clip: _clip,
profile,
avatarUrl: _clip.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -724,7 +737,6 @@ export class ClientServerService {
if (post) {
const _post = await this.galleryPostEntityService.pack(post);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -734,7 +746,7 @@ export class ClientServerService {
post: _post,
profile,
avatarUrl: _post.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -749,11 +761,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', {
channel: _channel,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -768,11 +779,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', {
game: _game,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -780,7 +790,7 @@ export class ClientServerService {
});
//#endregion
- //region noindex pages
+ //#region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
@@ -790,21 +800,97 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
- //endregion
+ //#endregion
+
+ //#region embed pages
+ fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ const user = await this.usersRepository.findOneBy({
+ id: request.params.user,
+ });
+
+ if (user == null) return;
+ if (user.host != null) return;
+
+ const _user = await this.userEntityService.pack(user);
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Sharkey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ user: _user,
+ }),
+ });
+ });
+
+ fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ const note = await this.notesRepository.findOneBy({
+ id: request.params.note,
+ });
+
+ if (note == null) return;
+ if (note.visibility !== 'public') return;
+ if (note.userHost != null) return;
+
+ const _note = await this.noteEntityService.pack(note, null, { detail: true });
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Sharkey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ note: _note,
+ }),
+ });
+ });
+
+ fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ const clip = await this.clipsRepository.findOneBy({
+ id: request.params.clip,
+ });
+
+ if (clip == null) return;
+
+ const _clip = await this.clipEntityService.pack(clip);
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Sharkey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ clip: _clip,
+ }),
+ });
+ });
+
+ fastify.get('/embed/*', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Sharkey',
+ ...await this.generateCommonPugData(this.meta),
+ });
+ });
fastify.get('/_info_card_', async (request, reply) => {
- const meta = await this.metaService.fetch(true);
-
reply.removeHeader('X-Frame-Options');
return await reply.view('info-card', {
version: this.config.version,
host: this.config.host,
- meta: meta,
+ meta: this.meta,
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
});
});
+ //#endregion
fastify.get('/bios', async (request, reply) => {
return await reply.view('bios', {
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index ef804b5bfd..981fbb4353 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly';
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.js';
import { query } from '@/misc/prelude/url.js';
@@ -32,7 +31,9 @@ export class UrlPreviewService {
@Inject(DI.redis)
private redisClient: Redis.Redis,
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@@ -75,9 +76,7 @@ export class UrlPreviewService {
return;
}
- const meta = await this.metaService.fetch();
-
- if (!meta.urlPreviewEnabled) {
+ if (!this.meta.urlPreviewEnabled) {
reply.code(403);
return {
error: new ApiError({
@@ -98,14 +97,14 @@ export class UrlPreviewService {
return cached;
}
- this.logger.info(meta.urlPreviewSummaryProxyUrl
+ this.logger.info(this.meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${key} ...`
: `Getting preview of ${key} ...`);
try {
- const summary = meta.urlPreviewSummaryProxyUrl
- ? await this.fetchSummaryFromProxy(url, meta, lang)
- : await this.fetchSummary(url, meta, lang);
+ const summary = this.meta.urlPreviewSummaryProxyUrl
+ ? await this.fetchSummaryFromProxy(url, this.meta, lang)
+ : await this.fetchSummary(url, this.meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js
index 9ff5dca72a..5dbb26f9e3 100644
--- a/packages/backend/src/server/web/bios.js
+++ b/packages/backend/src/server/web/bios.js
@@ -6,36 +6,6 @@
'use strict';
window.onload = async () => {
- const account = JSON.parse(localStorage.getItem('account'));
- const i = account.token;
-
- const api = (endpoint, data = {}) => {
- const promise = new Promise((resolve, reject) => {
- // Append a credential
- if (i) data.i = i;
-
- // Send request
- window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
- method: 'POST',
- body: JSON.stringify(data),
- credentials: 'omit',
- cache: 'no-cache'
- }).then(async (res) => {
- const body = res.status === 204 ? null : await res.json();
-
- if (res.status === 200) {
- resolve(body);
- } else if (res.status === 204) {
- resolve();
- } else {
- reject(body.error);
- }
- }).catch(reject);
- });
-
- return promise;
- };
-
const content = document.getElementById('content');
document.getElementById('ls').addEventListener('click', () => {
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
new file mode 100644
index 0000000000..b07dce3ac4
--- /dev/null
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -0,0 +1,208 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+ window.onerror = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED');
+ };
+ window.onunhandledrejection = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED_IN_PROMISE');
+ };
+
+ let forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ return;
+ }
+
+ // パラメータに応じてsplashのスタイルを変更
+ const params = new URLSearchParams(location.search);
+ if (params.has('rounded') && params.get('rounded') === 'false') {
+ document.documentElement.classList.add('norounded');
+ }
+ if (params.has('border') && params.get('border') === 'false') {
+ document.documentElement.classList.add('noborder');
+ }
+
+ // Force update when locales change
+ const langsVersion = LANGS_VERSION;
+ const localeVersion = localStorage.getItem('localeVersion');
+ if (localeVersion !== langsVersion) {
+ console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`);
+ localStorage.removeItem('localeVersion');
+ localStorage.removeItem('locale');
+ }
+
+ //#region Detect language & fetch translations
+ if (!localStorage.getItem('locale')) {
+ const supportedLangs = LANGS;
+ let lang = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
+ } else {
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
+ }
+ }
+
+ // for https://github.com/misskey-dev/misskey/issues/10202
+ if (lang == null || lang.toString == null || lang.toString() === 'null') {
+ console.error('invalid lang value detected!!!', typeof lang, lang);
+ lang = 'en-US';
+ }
+
+ const localRes = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`);
+ if (localRes.status === 200) {
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', await localRes.text());
+ localStorage.setItem('localeVersion', langsVersion);
+ } else {
+ renderError('LOCALE_FETCH');
+ return;
+ }
+ }
+ //#endregion
+
+ //#region Script
+ async function importAppScript() {
+ await import(`/embed_vite/${CLIENT_ENTRY}`)
+ .catch(async e => {
+ console.error(e);
+ renderError('APP_IMPORT');
+ });
+ }
+
+ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+ if (document.readyState !== 'loading') {
+ importAppScript();
+ } else {
+ window.addEventListener('DOMContentLoaded', () => {
+ importAppScript();
+ });
+ }
+ //#endregion
+
+ async function addStyle(styleText) {
+ let css = document.createElement('style');
+ css.appendChild(document.createTextNode(styleText));
+ document.head.appendChild(css);
+ }
+
+ async function renderError(code) {
+ // Cannot set property 'innerHTML' of null を回避
+ if (document.readyState === 'loading') {
+ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+ }
+ document.body.innerHTML = `
+ 読み込みに失敗しました
+ Failed to initialize Sharkey
+ Error Code: ${code}
+
+ リロード
+ Reload
+ `;
+ addStyle(`
+ #sharkey_app,
+ #splash {
+ display: none !important;
+ }
+
+ html,
+ body {
+ margin: 0;
+ }
+
+ body {
+ position: relative;
+ color: #dee7e4;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 24px;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ border-radius: var(--radius, 12px);
+ border: 1px solid rgba(231, 255, 251, 0.14);
+ }
+
+ body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #192320;
+ border-radius: var(--radius, 12px);
+ z-index: -1;
+ }
+
+ html.embed.norounded body,
+ html.embed.norounded body::before {
+ border-radius: 0;
+ }
+
+ html.embed.noborder body {
+ border: none;
+ }
+
+ .icon {
+ max-width: 60px;
+ width: 100%;
+ height: auto;
+ margin-bottom: 20px;
+ color: #dec340;
+ }
+
+ .message {
+ text-align: center;
+ font-size: 20px;
+ font-weight: 700;
+ margin-bottom: 20px;
+ }
+
+ .submessage {
+ text-align: center;
+ font-size: 90%;
+ margin-bottom: 7.5px;
+ }
+
+ .submessage:last-of-type {
+ margin-bottom: 20px;
+ }
+
+ button {
+ padding: 7px 14px;
+ min-width: 100px;
+ font-weight: 700;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ border-radius: 99rem;
+ background-color: #b4e900;
+ color: #192320;
+ border: none;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ button:hover {
+ background-color: #c6ff03;
+ }`);
+ }
+})();
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 38e37ce093..ad92480c1c 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-/**
- * BOOT LOADER
- * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
- * - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
- * - キャッシュされたコンパイル済みテーマを適用する。
- * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
- * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
- */
-
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@@ -33,8 +22,17 @@
return;
}
+ // Force update when locales change
+ const langsVersion = LANGS_VERSION;
+ const localeVersion = localStorage.getItem('localeVersion');
+ if (localeVersion !== langsVersion) {
+ console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`);
+ localStorage.removeItem('localeVersion');
+ localStorage.removeItem('locale');
+ }
+
//#region Detect language & fetch translations
- if (!localStorage.hasOwnProperty('locale')) {
+ if (!localStorage.getItem('locale')) {
const supportedLangs = LANGS;
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
@@ -48,37 +46,17 @@
}
}
- const metaRes = await window.fetch('/api/meta', {
- method: 'POST',
- body: JSON.stringify({}),
- credentials: 'omit',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- if (metaRes.status !== 200) {
- renderError('META_FETCH');
- return;
- }
- const meta = await metaRes.json();
- const v = meta.version;
- if (v == null) {
- renderError('META_FETCH_V');
- return;
- }
-
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
- const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+ const localRes = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`);
if (localRes.status === 200) {
localStorage.setItem('lang', lang);
localStorage.setItem('locale', await localRes.text());
- localStorage.setItem('localeVersion', v);
+ localStorage.setItem('localeVersion', langsVersion);
} else {
renderError('LOCALE_FETCH');
return;
@@ -110,7 +88,7 @@
const themeFontFaceName = 'sharkey-theme-font-face';
if (theme) {
let existingFontFace;
- document.fonts.forEach((v,k,s)=>{if (v.family === themeFontFaceName) existingFontFace=v;});
+ document.fonts.forEach((v) => { if (v.family === themeFontFaceName) existingFontFace = v;});
if (existingFontFace) document.fonts.delete(existingFontFace);
const themeProps = JSON.parse(theme);
@@ -124,8 +102,8 @@
document.fonts.add(fontFace);
fontFace.load().catch(
(failure) => {
- console.log(failure)
- }
+ console.log(failure);
+ },
);
}
for (const [k, v] of Object.entries(themeProps)) {
@@ -192,7 +170,7 @@
if (!errorsElement) {
document.body.innerHTML = `
-
+
@@ -202,10 +180,10 @@
Reload / リロード
The following actions may solve the problem. / 以下を行うと解決する可能性があります。
- Clear the browser cache / ブラウザのキャッシュをクリアする
Update your os and browser / ブラウザおよびOSを最新バージョンに更新する
Disable an adblocker / アドブロッカーを無効にする
- (Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する
+ Clear the browser cache / ブラウザのキャッシュをクリアする
+ (Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する
Other options / その他のオプション
@@ -238,7 +216,7 @@