summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-05-31 12:37:06 +0000
committerGitHub <noreply@github.com>2025-05-31 12:37:06 +0000
commit92b9a5218db145e9bb831cefd36c309de86083b5 (patch)
tree2ebad71633f9bbacabbc193254223146f1662aee /packages/backend/src/server/api
parentMerge pull request #15933 from misskey-dev/develop (diff)
parentRelease: 2025.5.1 (diff)
downloadmisskey-92b9a5218db145e9bb831cefd36c309de86083b5.tar.gz
misskey-92b9a5218db145e9bb831cefd36c309de86083b5.tar.bz2
misskey-92b9a5218db145e9bb831cefd36c309de86083b5.zip
Merge pull request #16005 from misskey-dev/develop
Release: 2025.5.1
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts22
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts108
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts5
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/admin/invite/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/jobs.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts113
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/queues.ts42
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/show-job.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/clips/notes.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/create.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/show.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/i/revoke-token.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/notes/children.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/mentions.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/renotes.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/replies.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts80
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts66
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/pages/show.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/users/followers.ts51
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts54
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users/recommendation.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.test.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts80
-rw-r--r--packages/backend/src/server/api/openapi/gen-spec.ts3
-rw-r--r--packages/backend/src/server/api/openapi/schemas.ts9
44 files changed, 783 insertions, 338 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index a42fdaf730..7a4af407a3 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
- await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
- if ('info' in err) {
- // errはLimiter.LimiterInfoであることが期待される
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- }, err.info);
- } else {
- throw new TypeError('information must be a rate-limiter information.');
- }
- });
+ const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
+ if (rateLimit != null) {
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, rateLimit.info);
+ }
}
}
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 52d73baa0a..a730d8c60e 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js';
+type RateLimitInfo = {
+ code: 'BRIEF_REQUEST_INTERVAL',
+ info: Limiter.LimiterInfo,
+} | {
+ code: 'RATE_LIMIT_EXCEEDED',
+ info: Limiter.LimiterInfo,
+};
+
@Injectable()
export class RateLimiterService {
private logger: Logger;
@@ -31,77 +39,55 @@ export class RateLimiterService {
}
@bindThis
- public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
- {
- if (this.disabled) {
- return Promise.resolve();
- }
-
- // Short-term limit
- const min = new Promise<void>((ok, reject) => {
- const minIntervalLimiter = new Limiter({
- id: `${actor}:${limitation.key}:min`,
- duration: limitation.minInterval! * factor,
- max: 1,
- db: this.redisClient,
- });
-
- minIntervalLimiter.get((err, info) => {
- if (err) {
- return reject({ code: 'ERR', info });
- }
+ private checkLimiter(options: Limiter.LimiterOption): Promise<Limiter.LimiterInfo> {
+ return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
+ new Limiter(options).get((err, info) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(info);
+ });
+ });
+ }
- this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
+ @bindThis
+ public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
+ if (this.disabled) {
+ return null;
+ }
- if (info.remaining === 0) {
- return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
- } else {
- if (hasLongTermLimit) {
- return max.then(ok, reject);
- } else {
- return ok();
- }
- }
- });
+ // Short-term limit
+ if (limitation.minInterval != null) {
+ const info = await this.checkLimiter({
+ id: `${actor}:${limitation.key}:min`,
+ duration: limitation.minInterval * factor,
+ max: 1,
+ db: this.redisClient,
});
- // Long term limit
- const max = new Promise<void>((ok, reject) => {
- const limiter = new Limiter({
- id: `${actor}:${limitation.key}`,
- duration: limitation.duration! * factor,
- max: limitation.max! / factor,
- db: this.redisClient,
- });
-
- limiter.get((err, info) => {
- if (err) {
- return reject({ code: 'ERR', info });
- }
+ this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
- this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
+ if (info.remaining === 0) {
+ return { code: 'BRIEF_REQUEST_INTERVAL', info };
+ }
+ }
- if (info.remaining === 0) {
- return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
- } else {
- return ok();
- }
- });
+ // Long term limit
+ if (limitation.duration != null && limitation.max != null) {
+ const info = await this.checkLimiter({
+ id: `${actor}:${limitation.key}`,
+ duration: limitation.duration,
+ max: limitation.max / factor,
+ db: this.redisClient,
});
- const hasShortTermLimit = typeof limitation.minInterval === 'number';
-
- const hasLongTermLimit =
- typeof limitation.duration === 'number' &&
- typeof limitation.max === 'number';
+ this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
- if (hasShortTermLimit) {
- return min;
- } else if (hasLongTermLimit) {
- return max;
- } else {
- return Promise.resolve();
+ if (info.remaining === 0) {
+ return { code: 'RATE_LIMIT_EXCEEDED', info };
}
}
+
+ return null;
}
}
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 1d983ca4bc..3e889372d8 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -89,10 +89,9 @@ export class SigninApiService {
return { error };
}
- try {
// not more than 1 attempt per second and not more than 10 attempts per hour
- await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
- } catch (err) {
+ const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ if (rateLimit != null) {
reply.code(429);
return {
error: {
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index e5170aa2dc..1fdd000fdf 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -175,6 +175,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js';
export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
export * as 'drive/files/show' from './endpoints/drive/files/show.js';
export * as 'drive/files/update' from './endpoints/drive/files/update.js';
+export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js';
export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
export * as 'drive/folders' from './endpoints/drive/folders.js';
export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
@@ -323,6 +324,7 @@ export * as 'notes/replies' from './endpoints/notes/replies.js';
export * as 'notes/search' from './endpoints/notes/search.js';
export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
export * as 'notes/show' from './endpoints/notes/show.js';
+export * as 'notes/show-partial-bulk' from './endpoints/notes/show-partial-bulk.js';
export * as 'notes/state' from './endpoints/notes/state.js';
export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index a7136d8c8c..b84a5c73f9 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -162,14 +162,21 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- fileId: { type: 'string', format: 'misskey:id' },
- url: { type: 'string' },
- },
anyOf: [
- { required: ['fileId'] },
- { required: ['url'] },
+ {
+ type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['fileId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ url: { type: 'string' },
+ },
+ required: ['url'],
+ },
],
} as const;
@@ -186,15 +193,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
- const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({
- where: [{
- url: ps.url,
- }, {
- thumbnailUrl: ps.url,
- }, {
- webpublicUrl: ps.url,
- }],
- });
+ const file = await this.driveFilesRepository.findOneBy(
+ 'fileId' in ps
+ ? { id: ps.fileId }
+ : [{ url: ps.url }, { thumbnailUrl: ps.url }, { webpublicUrl: ps.url }],
+ );
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 6834a6d213..7bde10af46 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -37,29 +37,45 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- id: { type: 'string', format: 'misskey:id' },
- name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
- fileId: { type: 'string', format: 'misskey:id' },
- category: {
- type: 'string',
- nullable: true,
- description: 'Use `null` to reset the category.',
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ id: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['id'],
+ },
+ {
+ type: 'object',
+ properties: {
+ name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
+ },
+ required: ['name'],
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ category: {
+ type: 'string',
+ nullable: true,
+ description: 'Use `null` to reset the category.',
+ },
+ aliases: { type: 'array', items: {
+ type: 'string',
+ } },
+ license: { type: 'string', nullable: true },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
+ type: 'string',
+ } },
+ },
},
- aliases: { type: 'array', items: {
- type: 'string',
- } },
- license: { type: 'string', nullable: true },
- isSensitive: { type: 'boolean' },
- localOnly: { type: 'boolean' },
- roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
- type: 'string',
- } },
- },
- anyOf: [
- { required: ['id'] },
- { required: ['name'] },
],
} as const;
@@ -78,10 +94,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
- // JSON schemeのanyOfの型変換がうまくいっていないらしい
- const required = { id: ps.id, name: ps.name } as
- | { id: MiEmoji['id']; name?: string }
- | { id?: MiEmoji['id']; name: string };
+ const required = 'id' in ps
+ ? { id: ps.id, name: 'name' in ps ? ps.name as string : undefined }
+ : { name: ps.name };
const error = await this.customEmojiService.update({
...required,
diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 5ecae3161a..e52b177e2b 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
+ createdBy: me,
+ createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 4a106e7175..924163afbb 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -495,6 +495,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ urlPreviewAllowRedirect: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
urlPreviewTimeout: {
type: 'number',
optional: false, nullable: false,
@@ -546,6 +550,27 @@ export const meta = {
},
},
},
+ singleUserMode: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ ugcVisibilityForVisitor: {
+ type: 'string',
+ enum: ['all', 'local', 'none'],
+ optional: false, nullable: false,
+ },
+ proxyRemoteFiles: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ signToActivityPubGet: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ allowExternalApRedirect: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
},
} as const;
@@ -683,6 +708,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
+ urlPreviewAllowRedirect: instance.urlPreviewAllowRedirect,
urlPreviewTimeout: instance.urlPreviewTimeout,
urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
@@ -691,6 +717,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
federation: instance.federation,
federationHosts: instance.federationHosts,
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
+ singleUserMode: instance.singleUserMode,
+ ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor,
+ proxyRemoteFiles: instance.proxyRemoteFiles,
+ signToActivityPubGet: instance.signToActivityPubGet,
+ allowExternalApRedirect: instance.allowExternalApRedirect,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
index 79731c9786..a68e95bf3f 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,13 +13,22 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ optional: false, nullable: false,
+ ref: 'QueueJob',
+ },
+ },
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
- state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
+ state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } },
search: { type: 'string' },
},
required: ['queue', 'state'],
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts
index 10ce48332a..0098160165 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,118 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: QUEUE_TYPES,
+ },
+ qualifiedName: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ counts: {
+ type: 'object',
+ optional: false, nullable: false,
+ additionalProperties: {
+ type: 'number',
+ },
+ },
+ isPaused: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ metrics: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ completed: {
+ optional: false, nullable: false,
+ ref: 'QueueMetrics',
+ },
+ failed: {
+ optional: false, nullable: false,
+ ref: 'QueueMetrics',
+ },
+ },
+ },
+ db: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ version: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ mode: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['cluster', 'standalone', 'sentinel'],
+ },
+ runId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ processId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ port: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ os: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ uptime: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ memory: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ total: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ used: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ fragmentationRatio: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ peak: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ clients: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ blocked: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ connected: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ },
+ }
+ },
+ },
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts
index 3a38275f60..8d27e38c84 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,47 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: QUEUE_TYPES,
+ },
+ counts: {
+ type: 'object',
+ optional: false, nullable: false,
+ additionalProperties: {
+ type: 'number',
+ },
+ },
+ isPaused: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ metrics: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ completed: {
+ optional: false, nullable: false,
+ ref: 'QueueMetrics',
+ },
+ failed: {
+ optional: false, nullable: false,
+ ref: 'QueueMetrics',
+ },
+ },
+ },
+ },
+ },
+ },
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts
index 63747b5540..1735c22674 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,11 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
+
+ res: {
+ optional: false, nullable: false,
+ ref: 'QueueJob',
+ },
} as const;
export const paramDef = {
@@ -28,7 +32,6 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
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 31eeaa5e38..578aa2b662 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -170,6 +170,7 @@ export const paramDef = {
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
},
urlPreviewEnabled: { type: 'boolean' },
+ urlPreviewAllowRedirect: { type: 'boolean' },
urlPreviewTimeout: { type: 'integer' },
urlPreviewMaximumContentLength: { type: 'integer' },
urlPreviewRequireContentLength: { type: 'boolean' },
@@ -196,6 +197,14 @@ export const paramDef = {
required: ['software', 'versionRange'],
},
},
+ singleUserMode: { type: 'boolean' },
+ ugcVisibilityForVisitor: {
+ type: 'string',
+ enum: ['all', 'local', 'none'],
+ },
+ proxyRemoteFiles: { type: 'boolean' },
+ signToActivityPubGet: { type: 'boolean' },
+ allowExternalApRedirect: { type: 'boolean' },
},
required: [],
} as const;
@@ -656,6 +665,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.urlPreviewEnabled = ps.urlPreviewEnabled;
}
+ if (ps.urlPreviewAllowRedirect !== undefined) {
+ set.urlPreviewAllowRedirect = ps.urlPreviewAllowRedirect;
+ }
+
if (ps.urlPreviewTimeout !== undefined) {
set.urlPreviewTimeout = ps.urlPreviewTimeout;
}
@@ -690,6 +703,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
+ if (ps.singleUserMode !== undefined) {
+ set.singleUserMode = ps.singleUserMode;
+ }
+
+ if (ps.ugcVisibilityForVisitor !== undefined) {
+ set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor;
+ }
+
+ if (ps.proxyRemoteFiles !== undefined) {
+ set.proxyRemoteFiles = ps.proxyRemoteFiles;
+ }
+
+ if (ps.signToActivityPubGet !== undefined) {
+ set.signToActivityPubGet = ps.signToActivityPubGet;
+ }
+
+ if (ps.allowExternalApRedirect !== undefined) {
+ set.allowExternalApRedirect = ps.allowExternalApRedirect;
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index f37cdc6658..b2d9cea03c 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -111,11 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// 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);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 2401ab8208..46b050d4b4 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -121,12 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
- this.queryService.generateBlockedHostQueryForNote(query);
- this.queryService.generateSuspendedUserQueryForNote(query);
- if (me) {
- this.queryService.generateMutedUserQueryForNotes(query, me);
- this.queryService.generateBlockedUserQueryForNotes(query, me);
- }
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
//#endregion
return await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 33f32d1d8a..4869ffd402 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -91,6 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' });
+ this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
const notes = await query
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 11c255a361..7d5c0ccd4d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -63,6 +63,12 @@ export const meta = {
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
httpStatusCode: 413,
},
+
+ unallowedFileType: {
+ message: 'Cannot upload the file because it is an unallowed file type.',
+ code: 'UNALLOWED_FILE_TYPE',
+ id: '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea',
+ },
},
} as const;
@@ -123,6 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
+ if (err.id === 'bd71c601-f9b0-4808-9137-a330647ced9b') throw new ApiError(meta.errors.unallowedFileType);
}
throw new ApiError();
} finally {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts
new file mode 100644
index 0000000000..c8500895eb
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { DI } from '@/di-symbols.js';
+import { DriveService } from '@/core/DriveService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ tags: ['drive'],
+
+ requireCredential: true,
+
+ kind: 'write:drive',
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } },
+ folderId: { type: 'string', format: 'misskey:id', nullable: true },
+ },
+ required: ['fileIds'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private driveService: DriveService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts
index e8f4539d61..9a2e2c73e8 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/show.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts
@@ -43,14 +43,21 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- fileId: { type: 'string', format: 'misskey:id' },
- url: { type: 'string' },
- },
anyOf: [
- { required: ['fileId'] },
- { required: ['url'] },
+ {
+ type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['fileId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ url: { type: 'string' },
+ },
+ required: ['url'],
+ },
],
} as const;
@@ -64,21 +71,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
- let file: MiDriveFile | null = null;
-
- if (ps.fileId) {
- file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
- } else if (ps.url) {
- file = await this.driveFilesRepository.findOne({
- where: [{
- url: ps.url,
- }, {
- webpublicUrl: ps.url,
- }, {
- thumbnailUrl: ps.url,
- }],
- });
- }
+ const file = await this.driveFilesRepository.findOneBy(
+ 'fileId' in ps
+ ? { id: ps.fileId }
+ : [{ url: ps.url }, { webpublicUrl: ps.url }, { thumbnailUrl: ps.url }],
+ );
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
index c05ee93c6f..08f5e3a7a1 100644
--- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
@@ -15,14 +15,21 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- tokenId: { type: 'string', format: 'misskey:id' },
- token: { type: 'string', nullable: true },
- },
anyOf: [
- { required: ['tokenId'] },
- { required: ['token'] },
+ {
+ type: 'object',
+ properties: {
+ tokenId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['tokenId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ token: { type: 'string', nullable: true },
+ },
+ required: ['token'],
+ },
],
} as const;
@@ -33,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accessTokensRepository: AccessTokensRepository,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.tokenId) {
+ if ('tokenId' in ps) {
const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } });
if (tokenExist) {
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index 712a86eb13..d457ad1220 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -70,12 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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);
- }
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 8d38bb1c65..1c73edf08e 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -78,11 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- if (me) {
- this.queryService.generateMutedUserQueryForNotes(query, me);
- this.queryService.generateBlockedUserQueryForNotes(query, me);
- this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
- }
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
+ if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
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 6a3ee817e4..2c8459525a 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -243,10 +243,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.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
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 d1dc22f233..ee61ab43da 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -156,10 +156,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index c3722b1b5a..7ffc349db5 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -72,11 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateBlockedHostQueryForNote(query);
- this.queryService.generateSuspendedUserQueryForNote(query);
- this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
- this.queryService.generateBlockedUserQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index ce2435b8eb..fa2306c1bf 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -72,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
const renotes = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index f491cc38ab..9626947480 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -56,10 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
const timeline = await query.limit(ps.limit).getMany();
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 d0781bd8dd..51255f0bbf 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
@@ -28,38 +28,53 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- reply: { type: 'boolean', nullable: true, default: null },
- renote: { type: 'boolean', nullable: true, default: null },
- withFiles: {
- type: 'boolean',
- default: false,
- description: 'Only show notes that have attached files.',
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ tag: { type: 'string', minLength: 1 },
+ },
+ required: ['tag'],
+ },
+ {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'array',
+ description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
+ items: {
+ type: 'array',
+ items: {
+ type: 'string',
+ minLength: 1,
+ },
+ minItems: 1,
+ },
+ minItems: 1,
+ },
+ },
+ required: ['query'],
+ },
+ ],
},
- poll: { type: 'boolean', nullable: true, default: null },
- sinceId: { type: 'string', format: 'misskey:id' },
- untilId: { type: 'string', format: 'misskey:id' },
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
-
- tag: { type: 'string', minLength: 1 },
- query: {
- type: 'array',
- description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
- items: {
- type: 'array',
- items: {
- type: 'string',
- minLength: 1,
+ {
+ type: 'object',
+ properties: {
+ reply: { type: 'boolean', nullable: true, default: null },
+ renote: { type: 'boolean', nullable: true, default: null },
+ withFiles: {
+ type: 'boolean',
+ default: false,
+ description: 'Only show notes that have attached files.',
},
- minItems: 1,
+ poll: { type: 'boolean', nullable: true, default: null },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
- minItems: 1,
},
- },
- anyOf: [
- { required: ['tag'] },
- { required: ['query'] },
],
} as const;
@@ -81,18 +96,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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);
+ this.queryService.generateBaseNoteFilteringQuery(query, me);
try {
- if (ps.tag) {
+ if ('tag' in ps) {
if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] });
} else {
query.andWhere(new Brackets(qb => {
- for (const tags of ps.query!) {
+ for (const tags of ps.query) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
diff --git a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts
new file mode 100644
index 0000000000..e102bc1d4a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: false,
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ reactions: {
+ type: 'object',
+ optional: false, nullable: false,
+ additionalProperties: {
+ type: 'number',
+ },
+ },
+ reactionEmojis: {
+ type: 'object',
+ optional: false, nullable: false,
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' }, maxItems: 100, minItems: 1 },
+ },
+ required: ['noteIds'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private noteEntityService: NoteEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ return await this.noteEntityService.fetchDiffs(ps.noteIds);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index 11839bce36..b93c73b0c5 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-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 { GetterService } from '@/server/api/GetterService.js';
+import { DI } from '@/di-symbols.js';
+import { MiMeta } from '@/models/Meta.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -46,6 +48,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private noteEntityService: NoteEntityService,
private getterService: GetterService,
) {
@@ -59,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.signinRequired);
}
+ if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
+ throw new ApiError(meta.errors.signinRequired);
+ }
+
+ if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
+ throw new ApiError(meta.errors.signinRequired);
+ }
+
return await this.noteEntityService.pack(note, me, {
detail: true,
});
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index e6d6a1b629..c76cca1518 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -199,10 +199,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.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
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 ec7c4b0f97..614cd9204d 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
@@ -184,10 +184,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.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts
index e08b832a3f..8427bab2d5 100644
--- a/packages/backend/src/server/api/endpoints/pages/show.ts
+++ b/packages/backend/src/server/api/endpoints/pages/show.ts
@@ -33,15 +33,22 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- pageId: { type: 'string', format: 'misskey:id' },
- name: { type: 'string' },
- username: { type: 'string' },
- },
anyOf: [
- { required: ['pageId'] },
- { required: ['name', 'username'] },
+ {
+ type: 'object',
+ properties: {
+ pageId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['pageId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ username: { type: 'string' },
+ },
+ required: ['name', 'username'],
+ },
],
} as const;
@@ -59,9 +66,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
let page: MiPage | null = null;
- if (ps.pageId) {
+ if ('pageId' in ps) {
page = await this.pagesRepository.findOneBy({ id: ps.pageId });
- } else if (ps.name && ps.username) {
+ } else {
const author = await this.usersRepository.findOneBy({
host: IsNull(),
usernameLower: ps.username.toLowerCase(),
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index 16b0783a01..e8a760e9f8 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -102,10 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
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.generateBaseNoteFilteringQuery(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index a8b4319a61..bb8d4c49e9 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -47,23 +47,38 @@ export const meta = {
} 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 },
-
- userId: { type: 'string', format: 'misskey:id' },
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ username: { type: 'string' },
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
+ },
+ required: ['username', 'host'],
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
},
- },
- anyOf: [
- { required: ['userId'] },
- { required: ['username', 'host'] },
],
} as const;
@@ -85,9 +100,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy(ps.userId != null
+ const user = await this.usersRepository.findOneBy('userId' in ps
? { id: ps.userId }
- : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
+ : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index feda5bb353..1fc87151b2 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -54,25 +54,39 @@ export const meta = {
} 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 },
-
- userId: { type: 'string', format: 'misskey:id' },
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ username: { type: 'string' },
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
+ },
+ required: ['username', 'host'],
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ birthday: { ...birthdaySchema, nullable: true },
+ },
},
-
- birthday: { ...birthdaySchema, nullable: true },
- },
- anyOf: [
- { required: ['userId'] },
- { required: ['username', 'host'] },
],
} as const;
@@ -94,9 +108,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy(ps.userId != null
+ const user = await this.usersRepository.findOneBy('userId' in ps
? { id: ps.userId }
- : { usernameLower: ps.username!.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
+ : { usernameLower: ps.username.toLowerCase(), host: this.utilityService.toPunyNullable(ps.host) ?? IsNull() });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 0c64df569d..5832790a61 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -186,12 +186,10 @@ 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);
- }
+ this.queryService.generateBaseNoteFilteringQuery(query, me, {
+ excludeAuthor: true,
+ excludeUserFromMute: ps.userId,
+ });
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
index 5b1c6b514b..769a72d7a1 100644
--- a/packages/backend/src/server/api/endpoints/users/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -64,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQueryForUsers(query, me);
this.queryService.generateBlockQueryForUsers(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 1d75437b81..f146095cf1 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -114,7 +114,7 @@ export const paramDef = {
type: 'object',
properties: {
userId: {
- anyOf: [
+ oneOf: [
{ type: 'string', format: 'misskey:id' },
{
type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 134f1a8e87..d1d6354d53 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -26,17 +26,32 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- detail: { type: 'boolean', default: true },
-
- username: { type: 'string', nullable: true },
- host: { type: 'string', nullable: true },
- },
- anyOf: [
- { required: ['username'] },
- { required: ['host'] },
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ username: { type: 'string', nullable: true },
+ },
+ required: ['username'],
+ },
+ {
+ type: 'object',
+ properties: {
+ host: { type: 'string', nullable: true },
+ },
+ required: ['host'],
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ detail: { type: 'boolean', default: true },
+ },
+ },
],
} as const;
@@ -47,8 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, (ps, me) => {
return this.userSearchService.searchByUsernameAndHost({
- username: ps.username,
- host: ps.host,
+ username: 'username' in ps ? ps.username : undefined,
+ host: 'host' in ps ? ps.host : undefined,
}, {
limit: ps.limit,
detail: ps.detail,
diff --git a/packages/backend/src/server/api/endpoints/users/show.test.ts b/packages/backend/src/server/api/endpoints/users/show.test.ts
new file mode 100644
index 0000000000..068ffd8bc9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/show.test.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+process.env.NODE_ENV = 'test';
+
+import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
+import { paramDef } from './show.js';
+
+const VALID = true;
+const INVALID = false;
+
+describe('api:users/show', () => {
+ describe('validation', () => {
+ const v = getValidator(paramDef);
+
+ test('Reject empty', () => expect(v({})).toBe(INVALID));
+ test('Reject host only', () => expect(v({ host: 'misskey.test' })).toBe(INVALID));
+ test('Accept userId only', () => expect(v({ userId: '1' })).toBe(VALID));
+ test('Accept username and host', () => expect(v({ username: 'alice', host: 'misskey.test' })).toBe(VALID));
+ });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 062326e28d..d57db42e6d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -5,7 +5,7 @@
import { In, IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -59,29 +59,53 @@ export const meta = {
} as const;
export const paramDef = {
- type: 'object',
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- userIds: { type: 'array', uniqueItems: true, items: {
- type: 'string', format: 'misskey:id',
- } },
- username: { type: 'string' },
- host: {
- type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
+ allOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+ },
+ {
+ type: 'object',
+ properties: {
+ userIds: { type: 'array', uniqueItems: true, items: {
+ type: 'string', format: 'misskey:id',
+ } },
+ },
+ required: ['userIds'],
+ },
+ {
+ type: 'object',
+ properties: {
+ username: { type: 'string' },
+ },
+ required: ['username'],
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ host: {
+ type: 'string',
+ nullable: true,
+ description: 'The local host is represented with `null`.',
+ },
+ },
},
- },
- anyOf: [
- { required: ['userId'] },
- { required: ['userIds'] },
- { required: ['username'] },
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -92,12 +116,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apiLoggerService: ApiLoggerService,
) {
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
+ if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
let user;
const isModerator = await this.roleService.isModerator(me);
- ps.username = ps.username?.trim();
+ if ('username' in ps) {
+ ps.username = ps.username.trim();
+ }
- if (ps.userIds) {
+ if ('userIds' in ps) {
if (ps.userIds.length === 0) {
return [];
}
@@ -122,13 +152,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
- if (typeof ps.host === 'string' && typeof ps.username === 'string') {
+ if (typeof ps.host === 'string' && 'username' in ps) {
+ if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => {
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`);
throw new ApiError(meta.errors.failedToResolveRemoteUser);
});
} else {
- const q: FindOptionsWhere<MiUser> = ps.userId != null
+ const q: FindOptionsWhere<MiUser> = 'userId' in ps
? { id: ps.userId }
: { usernameLower: ps.username!.toLowerCase(), host: IsNull() };
@@ -139,6 +173,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchUser);
}
+ if (this.serverSettings.ugcVisibilityForVisitor === 'local' && user.host != null && me == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
if (user.host == null) {
if (me == null && ip != null) {
this.perUserPvChart.commitByVisitor(user, ip);
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index ea64e32ee6..e1dead07cf 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -89,7 +89,8 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
schema.required = undefined;
}
- const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
+ const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1)
+ || ['allOf', 'oneOf', 'anyOf'].some(o => (Array.isArray(schema[o]) && schema[o].length >= 0));
const info = {
operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index c80dda8d96..1cdcbebd1a 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -38,14 +38,13 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're
if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) {
const $ref = `#/components/schemas/${schema.ref}`;
- if (schema.nullable || schema.optional) {
- res.allOf = [{ $ref }];
+ if (schema.nullable) {
+ res.oneOf = [{ $ref }, { type: 'null' }];
} else {
res.$ref = $ref;
}
- }
-
- if (schema.nullable) {
+ delete res.type;
+ } else if (schema.nullable) {
if (Array.isArray(schema.type) && !schema.type.includes('null')) {
res.type.push('null');
} else if (typeof schema.type === 'string') {