summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-05-07 02:46:42 +0000
committerGitHub <noreply@github.com>2025-05-07 02:46:42 +0000
commit9ed0d5ccecb53c973cef1e762dd0fae9e04f9a5b (patch)
treec41c3ee20b995c3a74a75d4005ab980d217a3727 /packages/backend/src
parentMerge pull request #15842 from misskey-dev/develop (diff)
parentRelease: 2025.5.0 (diff)
downloadmisskey-9ed0d5ccecb53c973cef1e762dd0fae9e04f9a5b.tar.gz
misskey-9ed0d5ccecb53c973cef1e762dd0fae9e04f9a5b.tar.bz2
misskey-9ed0d5ccecb53c973cef1e762dd0fae9e04f9a5b.zip
Merge pull request #15933 from misskey-dev/develop
Release: 2025.5.0
Diffstat (limited to '')
-rw-r--r--packages/backend/src/GlobalModule.ts9
-rw-r--r--packages/backend/src/core/AchievementService.ts82
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts18
-rw-r--r--packages/backend/src/core/QueryService.ts61
-rw-r--r--packages/backend/src/core/SearchService.ts9
-rw-r--r--packages/backend/src/core/UtilityService.ts20
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts5
-rw-r--r--packages/backend/src/misc/json-schema.ts3
-rw-r--r--packages/backend/src/models/Meta.ts10
-rw-r--r--packages/backend/src/models/Note.ts11
-rw-r--r--packages/backend/src/models/UserProfile.ts83
-rw-r--r--packages/backend/src/models/json-schema/achievement.ts25
-rw-r--r--packages/backend/src/models/json-schema/federation-instance.ts2
-rw-r--r--packages/backend/src/models/json-schema/notification.ts5
-rw-r--r--packages/backend/src/models/json-schema/user.ts13
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts13
-rw-r--r--packages/backend/src/server/ServerService.ts16
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts166
-rw-r--r--packages/backend/src/server/api/endpoint-base.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts19
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/channels/followed.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/clips/notes.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/notes/children.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/mentions.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/renotes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/replies.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/users/achievements.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users/featured-notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/reactions.ts8
42 files changed, 385 insertions, 261 deletions
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 5544eeeddd..435bd8dd45 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -24,8 +24,13 @@ const $config: Provider = {
const $db: Provider = {
provide: DI.db,
useFactory: async (config) => {
- const db = createPostgresDataSource(config);
- return await db.initialize();
+ try {
+ const db = createPostgresDataSource(config);
+ return await db.initialize();
+ } catch (e) {
+ console.log(e);
+ throw e;
+ }
},
inject: [DI.config],
};
diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts
index 4fc1193f32..8d2de89efd 100644
--- a/packages/backend/src/core/AchievementService.ts
+++ b/packages/backend/src/core/AchievementService.ts
@@ -9,87 +9,7 @@ import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from '@/core/NotificationService.js';
-
-export const ACHIEVEMENT_TYPES = [
- 'notes1',
- 'notes10',
- 'notes100',
- 'notes500',
- 'notes1000',
- 'notes5000',
- 'notes10000',
- 'notes20000',
- 'notes30000',
- 'notes40000',
- 'notes50000',
- 'notes60000',
- 'notes70000',
- 'notes80000',
- 'notes90000',
- 'notes100000',
- 'login3',
- 'login7',
- 'login15',
- 'login30',
- 'login60',
- 'login100',
- 'login200',
- 'login300',
- 'login400',
- 'login500',
- 'login600',
- 'login700',
- 'login800',
- 'login900',
- 'login1000',
- 'passedSinceAccountCreated1',
- 'passedSinceAccountCreated2',
- 'passedSinceAccountCreated3',
- 'loggedInOnBirthday',
- 'loggedInOnNewYearsDay',
- 'noteClipped1',
- 'noteFavorited1',
- 'myNoteFavorited1',
- 'profileFilled',
- 'markedAsCat',
- 'following1',
- 'following10',
- 'following50',
- 'following100',
- 'following300',
- 'followers1',
- 'followers10',
- 'followers50',
- 'followers100',
- 'followers300',
- 'followers500',
- 'followers1000',
- 'collectAchievements30',
- 'viewAchievements3min',
- 'iLoveMisskey',
- 'foundTreasure',
- 'client30min',
- 'client60min',
- 'noteDeletedWithin1min',
- 'postedAtLateNight',
- 'postedAt0min0sec',
- 'selfQuote',
- 'htl20npm',
- 'viewInstanceChart',
- 'outputHelloWorldOnScratchpad',
- 'open3windows',
- 'driveFolderCircularReference',
- 'reactWithoutRead',
- 'clickedClickHere',
- 'justPlainLucky',
- 'setNameToSyuilo',
- 'cookieClicked',
- 'brainDiver',
- 'smashTestNotificationButton',
- 'tutorialCompleted',
- 'bubbleGameExplodingHead',
- 'bubbleGameDoubleExplodingHead',
-] as const;
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
@Injectable()
export class AchievementService {
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 1ffeb4b3a4..6253f792ed 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -36,6 +36,7 @@ type TimelineOptions = {
excludeNoFiles?: boolean;
excludeReplies?: boolean;
excludePureRenotes: boolean;
+ ignoreAuthorFromUserSuspension?: boolean;
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
};
@@ -139,6 +140,23 @@ export class FanoutTimelineEndpointService {
};
}
+ {
+ const parentFilter = filter;
+ filter = (note) => {
+ const noteJoined = note as MiNote & {
+ renoteUser: MiUser | null;
+ replyUser: MiUser | null;
+ };
+ if (!ps.ignoreAuthorFromUserSuspension) {
+ if (note.user!.isSuspended) return false;
+ }
+ if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false;
+ if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false;
+
+ return parentFilter(note);
+ };
+ }
+
const redisTimeline: MiNote[] = [];
let readFromRedis = 0;
let lastSuccessfulRate = 1; // rateをキャッシュする?
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index 119eb49c02..b9cef5b0ec 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -43,29 +43,36 @@ export class QueryService {
) {
}
- public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
+ public makePaginationQuery<T extends ObjectLiteral>(
+ q: SelectQueryBuilder<T>,
+ sinceId?: string | null,
+ untilId?: string | null,
+ sinceDate?: number | null,
+ untilDate?: number | null,
+ targetColumn = 'id',
+ ): SelectQueryBuilder<T> {
if (sinceId && untilId) {
- q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
- q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
- q.orderBy(`${q.alias}.id`, 'DESC');
+ q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId });
+ q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceId) {
- q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
- q.orderBy(`${q.alias}.id`, 'ASC');
+ q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'ASC');
} else if (untilId) {
- q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
- q.orderBy(`${q.alias}.id`, 'DESC');
+ q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceDate && untilDate) {
- q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
- q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
- q.orderBy(`${q.alias}.id`, 'DESC');
+ q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
+ q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceDate) {
- q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
- q.orderBy(`${q.alias}.id`, 'ASC');
+ q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'ASC');
} else if (untilDate) {
- q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
- q.orderBy(`${q.alias}.id`, 'DESC');
+ q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) });
+ q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else {
- q.orderBy(`${q.alias}.id`, 'DESC');
+ q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
}
return q;
}
@@ -287,4 +294,26 @@ export class QueryService {
.andWhere(instanceSuspension('renoteUser'));
}
}
+
+ // Requirements: user replyUser renoteUser must be joined
+ @bindThis
+ public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
+ if (excludeAuthor) {
+ const brakets = (user: string) => new Brackets(qb => qb
+ .where(`note.${user}Id IS NULL`)
+ .orWhere(`user.id = ${user}.id`)
+ .orWhere(`${user}.isSuspended = FALSE`));
+ q
+ .andWhere(brakets('replyUser'))
+ .andWhere(brakets('renoteUser'));
+ } else {
+ const brakets = (user: string) => new Brackets(qb => qb
+ .where(`note.${user}Id IS NULL`)
+ .orWhere(`${user}.isSuspended = FALSE`));
+ q
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere(brakets('replyUser'))
+ .andWhere(brakets('renoteUser'));
+ }
+ }
}
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index d94281920e..20a776ded8 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -235,6 +235,7 @@ export class SearchService {
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
@@ -297,11 +298,17 @@ export class SearchService {
])
: [new Set<string>(), new Set<string>()];
- const query = this.notesRepository.createQueryBuilder('note');
+ const query = this.notesRepository.createQueryBuilder('note')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) });
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
const notes = (await query.getMany()).filter(note => {
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 23fb928ac9..67ec6cc7b0 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -6,10 +6,12 @@
import { URL, domainToASCII } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
+import semver from 'semver';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
-import { MiMeta } from '@/models/Meta.js';
+import { MiMeta, SoftwareSuspension } from '@/models/Meta.js';
+import { MiInstance } from '@/models/Instance.js';
@Injectable()
export class UtilityService {
@@ -143,4 +145,20 @@ export class UtilityService {
const host = this.extractDbHost(uri);
return this.isFederationAllowedHost(host);
}
+
+ @bindThis
+ public isDeliverSuspendedSoftware(software: Pick<MiInstance, 'softwareName' | 'softwareVersion'>): SoftwareSuspension | undefined {
+ if (software.softwareName == null) return undefined;
+ if (software.softwareVersion == null) {
+ // software version is null; suspend iff versionRange is *
+ return this.meta.deliverSuspendedSoftware.find(x =>
+ x.software === software.softwareName
+ && x.versionRange.trim() === '*');
+ } else {
+ const softwareVersion = software.softwareVersion;
+ return this.meta.deliverSuspendedSoftware.find(x =>
+ x.software === software.softwareName
+ && semver.satisfies(softwareVersion, x.versionRange, { includePrerelease: true }));
+ }
+ }
}
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 284537b986..3688cfb363 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -31,6 +31,7 @@ export class InstanceEntityService {
me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> {
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
+ const softwareSuspended = this.utilityService.isDeliverSuspendedSoftware(instance);
return {
id: instance.id,
@@ -41,8 +42,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount,
followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding,
- isSuspended: instance.suspensionState !== 'none',
- suspensionState: instance.suspensionState,
+ isSuspended: instance.suspensionState !== 'none' || Boolean(softwareSuspended),
+ suspensionState: instance.suspensionState === 'none' && softwareSuspended ? 'softwareSuspended' : instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 27aa3d89de..e4eb10efca 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -67,6 +67,7 @@ import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessage
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
+import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -78,6 +79,8 @@ export const refs = {
User: packedUserSchema,
UserList: packedUserListSchema,
+ Achievement: packedAchievementSchema,
+ AchievementName: packedAchievementNameSchema,
Ad: packedAdSchema,
Announcement: packedAnnouncementSchema,
App: packedAppSchema,
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 1fbf5371bc..46f3b2e3c0 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -664,4 +664,14 @@ export class MiMeta {
nullable: true,
})
public googleAnalyticsMeasurementId: string | null;
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public deliverSuspendedSoftware: SoftwareSuspension[];
}
+
+export type SoftwareSuspension = {
+ software: string,
+ versionRange: string,
+};
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index c5ca2b5776..3dcbdb735b 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -10,6 +10,16 @@ import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import type { MiDriveFile } from './DriveFile.js';
+// Note: When you create a new index for existing column of this table,
+// it might be better to index concurrently under isConcurrentIndexMigrationEnabled flag
+// by editing generated migration file since this table is very large,
+// and it will make a long lock to create index in most cases.
+// Please note that `CREATE INDEX CONCURRENTLY` is not supported in transaction,
+// so you need to set `transaction = false` in migration if isConcurrentIndexMigrationEnabled() is true.
+// Please refer 1745378064470-composite-note-index.js for example.
+// You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail
+// because it will always run CREATE INDEX in transaction based on decorators.
+// Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production,
@Index(['userId', 'id'])
@Entity('note')
export class MiNote {
@@ -229,7 +239,6 @@ export class MiNote {
comment: '[Denormalized]',
})
public renoteUserHost: string | null;
- //#endregion
constructor(data: Partial<MiNote>) {
if (data == null) return;
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 5544555296..c4c1fa5ec9 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -274,7 +274,7 @@ export class MiUserProfile {
default: [],
})
public achievements: {
- name: string;
+ name: typeof ACHIEVEMENT_TYPES[number];
unlockedAt: number;
}[];
@@ -295,3 +295,84 @@ export class MiUserProfile {
}
}
}
+
+export const ACHIEVEMENT_TYPES = [
+ 'notes1',
+ 'notes10',
+ 'notes100',
+ 'notes500',
+ 'notes1000',
+ 'notes5000',
+ 'notes10000',
+ 'notes20000',
+ 'notes30000',
+ 'notes40000',
+ 'notes50000',
+ 'notes60000',
+ 'notes70000',
+ 'notes80000',
+ 'notes90000',
+ 'notes100000',
+ 'login3',
+ 'login7',
+ 'login15',
+ 'login30',
+ 'login60',
+ 'login100',
+ 'login200',
+ 'login300',
+ 'login400',
+ 'login500',
+ 'login600',
+ 'login700',
+ 'login800',
+ 'login900',
+ 'login1000',
+ 'passedSinceAccountCreated1',
+ 'passedSinceAccountCreated2',
+ 'passedSinceAccountCreated3',
+ 'loggedInOnBirthday',
+ 'loggedInOnNewYearsDay',
+ 'noteClipped1',
+ 'noteFavorited1',
+ 'myNoteFavorited1',
+ 'profileFilled',
+ 'markedAsCat',
+ 'following1',
+ 'following10',
+ 'following50',
+ 'following100',
+ 'following300',
+ 'followers1',
+ 'followers10',
+ 'followers50',
+ 'followers100',
+ 'followers300',
+ 'followers500',
+ 'followers1000',
+ 'collectAchievements30',
+ 'viewAchievements3min',
+ 'iLoveMisskey',
+ 'foundTreasure',
+ 'client30min',
+ 'client60min',
+ 'noteDeletedWithin1min',
+ 'postedAtLateNight',
+ 'postedAt0min0sec',
+ 'selfQuote',
+ 'htl20npm',
+ 'viewInstanceChart',
+ 'outputHelloWorldOnScratchpad',
+ 'open3windows',
+ 'driveFolderCircularReference',
+ 'reactWithoutRead',
+ 'clickedClickHere',
+ 'justPlainLucky',
+ 'setNameToSyuilo',
+ 'cookieClicked',
+ 'brainDiver',
+ 'smashTestNotificationButton',
+ 'tutorialCompleted',
+ 'bubbleGameExplodingHead',
+ 'bubbleGameDoubleExplodingHead',
+] as const;
diff --git a/packages/backend/src/models/json-schema/achievement.ts b/packages/backend/src/models/json-schema/achievement.ts
new file mode 100644
index 0000000000..39a621a570
--- /dev/null
+++ b/packages/backend/src/models/json-schema/achievement.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
+
+export const packedAchievementNameSchema = {
+ type: 'string',
+ enum: ACHIEVEMENT_TYPES,
+ optional: false,
+} as const;
+
+export const packedAchievementSchema = {
+ type: 'object',
+ properties: {
+ name: {
+ ref: 'AchievementName',
+ },
+ unlockedAt: {
+ type: 'number',
+ optional: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index 912a0399d8..85f84952f1 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -48,7 +48,7 @@ export const packedFederationInstanceSchema = {
suspensionState: {
type: 'string',
nullable: false, optional: false,
- enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
+ enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding', 'softwareSuspended'],
},
isBlocked: {
type: 'boolean',
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 7f23d2d6a1..6de120c8d7 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import { notificationTypes, userExportableEntities } from '@/types.js';
const baseSchema = {
@@ -312,9 +311,7 @@ export const packedNotificationSchema = {
enum: ['achievementEarned'],
},
achievement: {
- type: 'string',
- optional: false, nullable: false,
- enum: ACHIEVEMENT_TYPES,
+ ref: 'AchievementName',
},
},
}, {
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index e475296702..2b5f706ff9 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -630,18 +630,7 @@ export const packedMeDetailedOnlySchema = {
type: 'array',
nullable: false, optional: false,
items: {
- type: 'object',
- nullable: false, optional: false,
- properties: {
- name: {
- type: 'string',
- nullable: false, optional: false,
- },
- unlockedAt: {
- type: 'number',
- nullable: false, optional: false,
- },
- },
+ ref: 'Achievement',
},
},
loggedInDays: {
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 5a16496011..391ccdac05 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -71,6 +71,15 @@ export class DeliverProcessorService {
return 'skip (suspended)';
}
+ const i = await (this.meta.enableStatsForFederatedInstances
+ ? this.federatedInstanceService.fetchOrRegister(host)
+ : this.federatedInstanceService.fetch(host));
+
+ // suspend server by software
+ if (i != null && this.utilityService.isDeliverSuspendedSoftware(i)) {
+ return 'skip (software suspended)';
+ }
+
try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
@@ -79,10 +88,6 @@ export class DeliverProcessorService {
// Update instance stats
process.nextTick(async () => {
- const i = await (this.meta.enableStatsForFederatedInstances
- ? this.federatedInstanceService.fetchOrRegister(host)
- : this.federatedInstanceService.fetch(host));
-
if (i == null) return;
if (i.isNotResponding) {
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 7decdd2c10..c859f1d82c 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -7,7 +7,7 @@ import cluster from 'node:cluster';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
-import Fastify, { FastifyInstance } from 'fastify';
+import Fastify, { type FastifyInstance } from 'fastify';
import fastifyStatic from '@fastify/static';
import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm';
@@ -73,7 +73,7 @@ export class ServerService implements OnApplicationShutdown {
}
@bindThis
- public async launch() {
+ public async launch(): Promise<void> {
const fastify = Fastify({
trustProxy: true,
logger: false,
@@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown {
reply.header('content-type', 'text/plain; charset=utf-8');
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
done(null, [
- 'Refusing to relay remote ActivityPub object lookup.',
- '',
+ "Refusing to relay remote ActivityPub object lookup.",
+ "",
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
].join('\n'));
});
@@ -301,7 +301,6 @@ export class ServerService implements OnApplicationShutdown {
}
await fastify.ready();
- return fastify;
}
@bindThis
@@ -310,6 +309,13 @@ export class ServerService implements OnApplicationShutdown {
await this.#fastify.close();
}
+ /**
+ * Get the Fastify instance for testing.
+ */
+ public get fastify(): FastifyInstance {
+ return this.#fastify;
+ }
+
@bindThis
async onApplicationShutdown(signal: string): Promise<void> {
await this.dispose();
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 960c7b5476..a42fdaf730 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -6,11 +6,8 @@
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
-import { Transform } from 'node:stream';
-import { type MultipartFile } from '@fastify/multipart';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
-import { AttachmentFile } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -19,7 +16,7 @@ import type Logger from '@/logger.js';
import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
-import { type RolePolicies, RoleService } from '@/core/RoleService.js';
+import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
@@ -203,6 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
+ 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<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@@ -217,7 +226,10 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
- this.call(endpoint, user, app, fields, multipartData, request).then((res) => {
+ this.call(endpoint, user, app, fields, {
+ name: multipartData.filename,
+ path: path,
+ }, request).then((res) => {
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
@@ -282,7 +294,10 @@ export class ApiCallService implements OnApplicationShutdown {
user: MiLocalUser | null | undefined,
token: MiAccessToken | null | undefined,
data: any,
- multipartFile: MultipartFile | null,
+ file: {
+ name: string;
+ path: string;
+ } | null,
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
) {
const isSecure = user != null && token == null;
@@ -356,37 +371,6 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- // Cast non JSON input
- if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
- for (const k of Object.keys(ep.params.properties)) {
- const param = ep.params.properties![k];
- if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
- try {
- data[k] = JSON.parse(data[k]);
- } catch (e) {
- throw new ApiError({
- message: 'Invalid param.',
- code: 'INVALID_PARAM',
- id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
- }, {
- param: k,
- reason: `cannot cast to ${param.type}`,
- });
- }
- }
- }
- }
-
- if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
- || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
- throw new ApiError({
- message: 'Your app does not have the necessary permissions to use this endpoint.',
- code: 'PERMISSION_DENIED',
- kind: 'permission',
- id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
- });
- }
-
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
@@ -420,89 +404,47 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- let attachmentFile: AttachmentFile | null = null;
- let cleanup = () => {};
- if (ep.meta.requireFile && request.method === 'POST' && multipartFile) {
- const policies = await this.roleService.getUserPolicies(user!.id);
- const result = await this.handleAttachmentFile(
- Math.min((policies.maxFileSizeMb * 1024 * 1024), this.config.maxFileSize),
- multipartFile,
- );
- attachmentFile = result.attachmentFile;
- cleanup = result.cleanup;
+ if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
+ || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
+ throw new ApiError({
+ message: 'Your app does not have the necessary permissions to use this endpoint.',
+ code: 'PERMISSION_DENIED',
+ kind: 'permission',
+ id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
+ });
+ }
+
+ // Cast non JSON input
+ if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
+ for (const k of Object.keys(ep.params.properties)) {
+ const param = ep.params.properties![k];
+ if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
+ try {
+ data[k] = JSON.parse(data[k]);
+ } catch (e) {
+ throw new ApiError({
+ message: 'Invalid param.',
+ code: 'INVALID_PARAM',
+ id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
+ }, {
+ param: k,
+ reason: `cannot cast to ${param.type}`,
+ });
+ }
+ }
+ }
}
// API invoking
if (this.config.sentryForBackend) {
return await Sentry.startSpan({
name: 'API: ' + ep.name,
- }, () => {
- return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
- .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
- .finally(() => cleanup());
- });
+ }, () => ep.exec(data, user, token, file, request.ip, request.headers)
+ .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
} else {
- return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
- .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
- .finally(() => cleanup());
- }
- }
-
- @bindThis
- private async handleAttachmentFile(
- fileSizeLimit: number,
- multipartFile: MultipartFile,
- ) {
- function createTooLongError() {
- return new ApiError({
- httpStatusCode: 413,
- kind: 'client',
- message: 'File size is too large.',
- code: 'FILE_SIZE_TOO_LARGE',
- id: 'ff827ce8-9b4b-4808-8511-422222a3362f',
- });
- }
-
- function createLimitStream(limit: number) {
- let total = 0;
-
- return new Transform({
- transform(chunk, _, callback) {
- total += chunk.length;
- if (total > limit) {
- callback(createTooLongError());
- } else {
- callback(null, chunk);
- }
- },
- });
+ return await ep.exec(data, user, token, file, request.ip, request.headers)
+ .catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
}
-
- const [path, cleanup] = await createTemp();
- try {
- await stream.pipeline(
- multipartFile.file,
- createLimitStream(fileSizeLimit),
- fs.createWriteStream(path),
- );
-
- // ファイルサイズが制限を超えていた場合
- // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
- if (multipartFile.file.truncated) {
- throw createTooLongError();
- }
- } catch (err) {
- cleanup();
- throw err;
- }
-
- return {
- attachmentFile: {
- name: multipartFile.filename,
- path,
- },
- cleanup,
- };
}
@bindThis
diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts
index b063487305..e061aa3a8e 100644
--- a/packages/backend/src/server/api/endpoint-base.ts
+++ b/packages/backend/src/server/api/endpoint-base.ts
@@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export type Response = Record<string, any> | void;
-export type AttachmentFile = {
+type File = {
name: string | null;
path: string;
};
// TODO: paramsの型をT['params']のスキーマ定義から推論する
type Executor<T extends IEndpointMeta, Ps extends Schema> =
- (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
- Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
+ (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
+ Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
- public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
+ public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef);
- this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
+ this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 53e2b2b237..4a106e7175 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -528,6 +528,24 @@ export const meta = {
optional: false, nullable: false,
},
},
+ deliverSuspendedSoftware: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ software: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ versionRange: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ },
},
},
} as const;
@@ -672,6 +690,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
federation: instance.federation,
federationHosts: instance.federationHosts,
+ deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
};
});
}
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 bc05587668..31eeaa5e38 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -185,6 +185,17 @@ export const paramDef = {
type: 'string',
},
},
+ deliverSuspendedSoftware: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ software: { type: 'string' },
+ versionRange: { type: 'string' },
+ },
+ required: ['software', 'versionRange'],
+ },
+ },
},
required: [],
} as const;
@@ -671,6 +682,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.federation = ps.federation;
}
+ if (ps.deliverSuspendedSoftware !== undefined) {
+ set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware;
+ }
+
if (Array.isArray(ps.federationHosts)) {
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 4708dab73c..f37cdc6658 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -112,6 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts
index d2f36f251e..294b5e4bc4 100644
--- a/packages/backend/src/server/api/endpoints/channels/followed.ts
+++ b/packages/backend/src/server/api/endpoints/channels/followed.ts
@@ -48,7 +48,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
+ const query = this.queryService
+ .makePaginationQuery(
+ this.channelFollowingsRepository.createQueryBuilder(),
+ ps.sinceId,
+ ps.untilId,
+ null,
+ null,
+ 'followeeId',
+ )
.andWhere({ followerId: me.id });
const followings = await query
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 620cdb0f5d..2401ab8208 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -122,6 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 2b65407cea..33f32d1d8a 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -85,9 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
+ this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ // this.queryService.generateSuspendedUserQueryForNote(query); // To avoid problems with removing notes, ignoring suspended user for now
if (me) {
- this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
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 17face8f82..11c255a361 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -61,6 +61,7 @@ export const meta = {
message: 'Cannot upload the file because it exceeds the maximum file size.',
code: 'MAX_FILE_SIZE_EXCEEDED',
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
+ httpStatusCode: 413,
},
},
} as const;
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 e70905ef1b..0e42647ef7 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -5,7 +5,8 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
+import { AchievementService } from '@/core/AchievementService.js';
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index 218a3c1a4c..712a86eb13 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -71,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index e7aba2d306..a57c84d432 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
const notes = (await query.getMany()).filter(note => {
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
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 39b519a599..6a3ee817e4 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -244,6 +244,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
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 8b2d5397b2..d1dc22f233 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -157,6 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index f5cddd5bad..c3722b1b5a 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index 178e311ed1..ce2435b8eb 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index d9aaed2f10..f491cc38ab 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -57,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
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 079231d432..d0781bd8dd 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
@@ -82,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 42752eaeec..e6d6a1b629 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -200,6 +200,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
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 58a4223207..ec7c4b0f97 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
@@ -185,6 +185,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index b0d3f6d2f9..16b0783a01 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -103,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts
index f7139b3684..bae216e347 100644
--- a/packages/backend/src/server/api/endpoints/users/achievements.ts
+++ b/packages/backend/src/server/api/endpoints/users/achievements.ts
@@ -14,15 +14,7 @@ export const meta = {
res: {
type: 'array',
items: {
- type: 'object',
- properties: {
- name: {
- type: 'string',
- },
- unlockedAt: {
- type: 'number',
- },
- },
+ ref: 'Achievement',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts
index 053fd60548..90bd11bc25 100644
--- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts
@@ -88,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
const notes = (await query.getMany()).filter(note => {
if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index b0585f75fc..0c64df569d 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -130,6 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
useDbFallback: true,
ignoreAuthorFromMute: true,
ignoreAuthorFromInstanceBlock: true,
+ ignoreAuthorFromUserSuspension: true,
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
excludePureRenotes: !ps.withRenotes,
@@ -186,6 +187,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query, true);
+ this.queryService.generateSuspendedUserQueryForNote(query, true);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index bb9000a7a0..d6f1ecd8ed 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -99,10 +99,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('reaction.userId = :userId', { userId: ps.userId })
- .leftJoinAndSelect('reaction.note', 'note');
+ .leftJoinAndSelect('reaction.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateSuspendedUserQueryForNote(query);
const reactions = (await query
.limit(ps.limit)