diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-04-29 15:37:43 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-04-29 15:54:11 -0400 |
| commit | 9c301fa5aac7e339a2b41feab8d0d247a60f50aa (patch) | |
| tree | 26e1423620a2811a5e9372bcee6858851d9fad3e /packages/backend/src | |
| parent | align `docker_example.yml` with `example.yml` (diff) | |
| parent | chore: follow up on fixing Chromatic CI diff strategy (#15912) (diff) | |
| download | sharkey-9c301fa5aac7e339a2b41feab8d0d247a60f50aa.tar.gz sharkey-9c301fa5aac7e339a2b41feab8d0d247a60f50aa.tar.bz2 sharkey-9c301fa5aac7e339a2b41feab8d0d247a60f50aa.zip | |
Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts:
# .github/workflows/api-misskey-js.yml
# .github/workflows/changelog-check.yml
# .github/workflows/check-misskey-js-autogen.yml
# .github/workflows/get-api-diff.yml
# .github/workflows/lint.yml
# .github/workflows/locale.yml
# .github/workflows/on-release-created.yml
# .github/workflows/storybook.yml
# .github/workflows/test-backend.yml
# .github/workflows/test-federation.yml
# .github/workflows/test-frontend.yml
# .github/workflows/test-misskey-js.yml
# .github/workflows/test-production.yml
# .github/workflows/validate-api-json.yml
# package.json
# packages/backend/package.json
# packages/backend/src/server/api/ApiCallService.ts
# packages/backend/src/server/api/endpoints/drive/files/create.ts
# packages/frontend-shared/js/url.ts
# packages/frontend/package.json
# packages/frontend/src/components/MkFileCaptionEditWindow.vue
# packages/frontend/src/components/MkInfo.vue
# packages/frontend/src/components/MkLink.vue
# packages/frontend/src/components/MkNote.vue
# packages/frontend/src/components/MkNotes.vue
# packages/frontend/src/components/MkPageWindow.vue
# packages/frontend/src/components/MkReactionsViewer.vue
# packages/frontend/src/components/MkTimeline.vue
# packages/frontend/src/components/MkUrlPreview.vue
# packages/frontend/src/components/MkUserPopup.vue
# packages/frontend/src/components/global/MkPageHeader.vue
# packages/frontend/src/components/global/MkUrl.vue
# packages/frontend/src/components/global/PageWithHeader.vue
# packages/frontend/src/pages/about-misskey.vue
# packages/frontend/src/pages/announcements.vue
# packages/frontend/src/pages/antenna-timeline.vue
# packages/frontend/src/pages/channel.vue
# packages/frontend/src/pages/instance-info.vue
# packages/frontend/src/pages/note.vue
# packages/frontend/src/pages/page.vue
# packages/frontend/src/pages/role.vue
# packages/frontend/src/pages/tag.vue
# packages/frontend/src/pages/timeline.vue
# packages/frontend/src/pages/user-list-timeline.vue
# packages/frontend/src/pages/user/followers.vue
# packages/frontend/src/pages/user/following.vue
# packages/frontend/src/pages/user/home.vue
# packages/frontend/src/pages/user/index.vue
# packages/frontend/src/ui/deck.vue
# packages/misskey-js/generator/package.json
# pnpm-lock.yaml
# scripts/changelog-checker/package-lock.json
# scripts/changelog-checker/package.json
Diffstat (limited to 'packages/backend/src')
38 files changed, 235 insertions, 90 deletions
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 4be1b0e41b..bb76b680a4 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -515,9 +515,16 @@ export class DriveService { const policies = await this.roleService.getUserPolicies(user.id); const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; + const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; this.registerLogger.debug('drive capacity override applied'); this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + if (maxFileSize < info.size) { + if (isLocalUser) { + throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.'); + } + } + // If usage limit exceeded if (driveCapacity < usage + info.size) { if (isLocalUser) { diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 84ca06ec1e..af2723e99d 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -8,10 +8,12 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; +import type { MiMeta } from '@/models/Meta.js'; import { Packed } from '@/misc/json-schema.js'; import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { CacheService } from '@/core/CacheService.js'; @@ -30,6 +32,7 @@ type TimelineOptions = { alwaysIncludeMyNotes?: boolean; ignoreAuthorFromBlock?: boolean; ignoreAuthorFromMute?: boolean; + ignoreAuthorFromInstanceBlock?: boolean; excludeNoFiles?: boolean; excludeReplies?: boolean; excludeBots?: boolean; @@ -43,9 +46,13 @@ export class FanoutTimelineEndpointService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.meta) + private meta: MiMeta, + private noteEntityService: NoteEntityService, private cacheService: CacheService, private fanoutTimelineService: FanoutTimelineService, + private utilityService: UtilityService, ) { } @@ -125,6 +132,19 @@ export class FanoutTimelineEndpointService { }; } + { + const parentFilter = filter; + filter = (note) => { + if (!ps.ignoreAuthorFromInstanceBlock) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, note.userHost)) return false; + } + if (note.userId !== note.renoteUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.renoteUserHost)) return false; + if (note.userId !== note.replyUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.replyUserHost)) return false; + + return parentFilter(note); + }; + } + const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 6f978b34c8..6f60475442 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -34,6 +34,7 @@ export const webpDefault: sharp.WebpOptions = { smartSubsample: true, mixed: true, effort: 2, + loop: 0, }; export const avifDefault: sharp.AvifOptions = { diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index c611fe5f1e..fb4335b55d 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import type { SelectQueryBuilder } from 'typeorm'; @@ -36,6 +36,9 @@ export class QueryService { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + @Inject(DI.meta) + private meta: MiMeta, + private idService: IdService, ) { } @@ -251,4 +254,37 @@ export class QueryService { q.setParameters(mutingQuery.getParameters()); } + + @bindThis + public generateBlockedHostQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void { + let nonBlockedHostQuery: (part: string) => string; + if (this.meta.blockedHosts.length === 0) { + nonBlockedHostQuery = () => '1=1'; + } else { + nonBlockedHostQuery = (match: string) => `${match} NOT ILIKE ALL(ARRAY[:...blocked])`; + q.setParameters({ blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }); + } + + if (excludeAuthor) { + const instanceSuspension = (user: string) => new Brackets(qb => qb + .where(`note.${user}Id IS NULL`) // no corresponding user + .orWhere(`note.userId = note.${user}Id`) + .orWhere(`note.${user}Host IS NULL`) // local + .orWhere(nonBlockedHostQuery(`note.${user}Host`))); + + q + .andWhere(instanceSuspension('replyUser')) + .andWhere(instanceSuspension('renoteUser')); + } else { + const instanceSuspension = (user: string) => new Brackets(qb => qb + .where(`note.${user}Id IS NULL`) // no corresponding user + .orWhere(`note.${user}Host IS NULL`) // local + .orWhere(nonBlockedHostQuery(`note.${user}Host`))); + + q + .andWhere(instanceSuspension('user')) + .andWhere(instanceSuspension('replyUser')) + .andWhere(instanceSuspension('renoteUser')); + } + } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 8b98680f4c..229781c079 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -48,6 +48,7 @@ export type RolePolicies = { canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; + maxFileSizeMb: number; alwaysMarkNsfw: boolean; canUpdateBioMedia: boolean; pinLimit: number; @@ -86,6 +87,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, + maxFileSizeMb: 10, alwaysMarkNsfw: false, canUpdateBioMedia: true, pinLimit: 5, @@ -399,6 +401,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), + maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index e17677bcb5..37238dc4b0 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -300,6 +300,7 @@ export class SearchService { } this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(query); if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); @@ -366,9 +367,14 @@ export class SearchService { this.cacheService.userBlockedCache.fetch(me.id), ]) : [new Set<string>(), new Set<string>()]; - const notes = (await this.notesRepository.findBy({ - id: In(res.hits.map(x => x.id)), - })).filter(note => { + + const query = this.notesRepository.createQueryBuilder('note'); + + query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) }); + + this.queryService.generateBlockedHostQueryForNote(query); + + const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; return true; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 9e95684f67..307c114c96 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + maxFileSizeMb: { + type: 'integer', + optional: false, nullable: false, + }, alwaysMarkNsfw: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index dce47e2290..5857b3059e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown { } @bindThis - public async launch(): Promise<void> { + public async launch() { const fastify = Fastify({ trustProxy: true, logger: false, @@ -135,8 +135,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')); }); @@ -304,6 +304,7 @@ export class ServerService implements OnApplicationShutdown { } await fastify.ready(); + return fastify; } @bindThis diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index b22a8c1837..1b8d33f9c9 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -6,8 +6,11 @@ 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'; @@ -16,7 +19,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 { RoleService } from '@/core/RoleService.js'; +import { type RolePolicies, RoleService } from '@/core/RoleService.js'; import type { Config } from '@/config.js'; import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; import { SkRateLimiterService } from '@/server/SkRateLimiterService.js'; @@ -191,18 +194,6 @@ 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,10 +208,7 @@ export class ApiCallService implements OnApplicationShutdown { return; } this.authenticateService.authenticate(token).then(([user, app]) => { - this.call(endpoint, user, app, fields, { - name: multipartData.filename, - path: path, - }, request, reply).then((res) => { + this.call(endpoint, user, app, fields, multipartData, request, reply).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { this.#sendApiError(reply, err); @@ -290,10 +278,7 @@ export class ApiCallService implements OnApplicationShutdown { user: MiLocalUser | null | undefined, token: MiAccessToken | null | undefined, data: any, - file: { - name: string; - path: string; - } | null, + multipartFile: MultipartFile | null, request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>, reply: FastifyReply, ) { @@ -369,6 +354,37 @@ 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)) { @@ -402,47 +418,89 @@ export class ApiCallService implements OnApplicationShutdown { } } - 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}`, - }); - } - } - } + 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; } // API invoking if (this.config.sentryForBackend) { return await Sentry.startSpan({ name: 'API: ' + ep.name, - }, () => ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); + }, () => { + return ep.exec(data, user, token, attachmentFile, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)) + .finally(() => cleanup()); + }); } else { - return await ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); + 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); + } + }, + }); } + + 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 e061aa3a8e..b063487305 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; -type File = { +export type AttachmentFile = { 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?: File, 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?: AttachmentFile, 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?: File, 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?: AttachmentFile, 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?: File, ip?: string | null, headers?: Record<string, string> | null) => { + 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) => { let cleanup: undefined | (() => void) = undefined; if (meta.requireFile) { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 57c62b7c89..b90ba6aa0d 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -117,6 +117,7 @@ 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.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 5fc0ae00b2..6336f43e9f 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -137,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + this.queryService.generateBlockedHostQueryForNote(query); if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts index 63b75fb6a7..52a054303b 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchMessage: { message: 'No such message.', diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts index 5f61e7e992..2197e7bf80 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchMessage: { message: 'No such message.', diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts index 6784bb6ecf..adfcd232f9 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchMessage: { message: 'No such message.', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts index 82a8e1f30d..1ea81448c1 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchRoom: { message: 'No such room.', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts index b8a228089b..88ea234527 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchRoom: { message: 'No such room.', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts index d561f9e03f..550b4da1a6 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchRoom: { message: 'No such room.', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts index a3ad0c2d6f..f99b408d67 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchRoom: { message: 'No such room.', diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts index 11cbe7b8b9..ee60f92505 100644 --- a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts +++ b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts @@ -16,9 +16,6 @@ export const meta = { kind: 'write:chat', - res: { - }, - errors: { noSuchRoom: { message: 'No such room.', diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 69ff45a1c6..59513e530d 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); + this.queryService.generateBlockedHostQueryForNote(query); if (me) { this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQueryForNotes(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 f67ff6ddc4..7043f4883a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -62,6 +62,12 @@ export const meta = { code: 'COMMENT_TOO_LONG', id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', }, + + maxFileSizeExceeded: { + message: 'Cannot upload the file because it exceeds the maximum file size.', + code: 'MAX_FILE_SIZE_EXCEEDED', + id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a', + }, }, } as const; @@ -128,6 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (err instanceof IdentifiableError) { 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); } throw new ApiError(); } finally { diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index c97a0c0bc7..8f19d534d4 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -79,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 4853489827..734ff31700 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; +import { QueryService } from '@/core/QueryService.js'; export const meta = { tags: ['notes'], @@ -58,6 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private cacheService: CacheService, private noteEntityService: NoteEntityService, private featuredService: FeaturedService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { let noteIds: string[]; @@ -100,6 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + this.queryService.generateBlockedHostQueryForNote(query); + const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; if (me && isUserRelated(note, userIdsWhoMeMuting)) 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 b0e3327411..6461a2e33f 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -254,6 +254,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 864592ed9b..f55853f3f3 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -167,6 +167,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 bfcd120f49..269b57366c 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -78,6 +78,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.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 01f09433a9..0f08cc9cf2 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 f04c9b0ec4..0882e19182 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -62,6 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 af9bc3b426..91874a8195 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 @@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE'); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 9f19117426..a2dfa7fdac 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -209,6 +209,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 4c37edfdec..60f18a09b0 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 @@ -190,6 +190,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 dd40e49d3d..d1c2e4b686 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -108,6 +108,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.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); 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 e6acae08b1..3fb091cc0e 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { CacheService } from '@/core/CacheService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; +import { QueryService } from '@/core/QueryService.js'; export const meta = { tags: ['notes'], @@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private featuredService: FeaturedService, private cacheService: CacheService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>(); @@ -91,6 +93,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + this.queryService.generateBlockedHostQueryForNote(query); + const notes = (await query.getMany()).filter(note => { if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false; if (me && isUserRelated(note, userIdsWhoMeMuting, true)) 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 a4eee544f0..965baa859a 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -145,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- redisTimelines, useDbFallback: true, ignoreAuthorFromMute: true, + ignoreAuthorFromInstanceBlock: true, excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, @@ -216,6 +217,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(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 49c1190197..56f59bd285 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -108,6 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reaction.note', 'note'); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateBlockedHostQueryForNote(query); const reactions = (await query .limit(ps.limit) diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 8094a0f6de..1c63d77e06 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -31,6 +31,7 @@ html { margin: auto; width: 64px; height: 64px; + border-radius: 10px; pointer-events: none; } diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css index 5e8786cc4e..0911d562bf 100644 --- a/packages/backend/src/server/web/style.embed.css +++ b/packages/backend/src/server/web/style.embed.css @@ -53,6 +53,7 @@ html.embed.noborder #splash { margin: auto; width: 64px; height: 64px; + border-radius: 10px; pointer-events: none; } |