diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f8d9905ecd..182ee2fbb2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "version": "8.9.2" }, "ghcr.io/devcontainers/features/node:1": { - "version": "20.10.0" + "version": "20.12.2" } }, "forwardPorts": [3000], diff --git a/.node-version b/.node-version index d5a159609d..87834047a6 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.10.0 +20.12.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 87db026183..bc00d5975c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## Unreleased +## 2024.5.0 ### Note - コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。 +- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。 ### General - Enhance: URLプレビューの有効化・無効化を設定できるように #13569 @@ -35,7 +36,10 @@ - Enhance: リプライにて引用がある場合テキストが空でもノートできるように - 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます - Enhance: フォローするかどうかの確認ダイアログを出せるように -- Chore: AiScriptを0.18.0にバージョンアップ +- Enhance: Playを手動でリロードできるように +- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように +- Enhance: `Ui:C:postForm` および `Ui:C:postFormButton` に `localOnly` と `visibility` を設定できるように +- Enhance: AiScriptを0.18.0にバージョンアップ - Fix: 一部のページ内リンクが正しく動作しない問題を修正 - Fix: 周年の実績が閏年を考慮しない問題を修正 - Fix: ローカルURLのプレビューポップアップが左上に表示される @@ -50,10 +54,18 @@ - Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正 - Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正 - Fix: ダイレクト投稿の宛先が保存されない問題を修正 +- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正 +- Fix: ページのOGP URLが間違っているのを修正 +- Fix: リバーシの対局を正しく共有できないことがある問題を修正 +- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正 +- Fix: 連合なしの状態の読み書きができない問題を修正 ### Server - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに - Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化) +- Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756) + - 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります +- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける - Fix: フォローリクエストを作成する際に既存のものは削除するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) - Fix: エンドポイント`notes/translate`のエラーを改善 @@ -62,10 +74,12 @@ - Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正 - Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) +- Fix: Add Cache-Control to Bull Board - Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正 - Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正 - Fix: グローバルタイムラインで返信が表示されないことがある問題を修正 - Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正 +- Fix: AP Link等は添付ファイル扱いしないようになど (#13754) ## 2024.3.1 diff --git a/Dockerfile b/Dockerfile index 7e0a778862..cee1f03535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.10.0-alpine3.18 +ARG NODE_VERSION=20.12.2-alpine3.19 FROM node:${NODE_VERSION} as build diff --git a/locales/index.d.ts b/locales/index.d.ts index fe24a2c44c..17ddbb3f63 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5149,6 +5149,10 @@ export interface Locale extends ILocale { * フォローの際常に確認する */ "alwaysConfirmFollow": string; + /** + * お問い合わせ + */ + "inquiry": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 28fac61731..b37ba2ec51 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1283,6 +1283,7 @@ keepOriginalFilename: "オリジナルのファイル名を保持" keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。" noDescription: "説明文はありません" alwaysConfirmFollow: "フォローの際常に確認する" +inquiry: "お問い合わせ" _bubbleGame: howToPlay: "遊び方" diff --git a/package.json b/package.json index 7791dfb949..f0a0ef50fe 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "sharkey", - "version": "2024.3.3-devel", + "version": "2024.5.0-beta.1", "codename": "shonk", "repository": { "type": "git", "url": "https://activitypub.software/TransFem-org/Sharkey.git" }, - "packageManager": "pnpm@8.15.4", + "packageManager": "pnpm@9.0.6", "workspaces": [ "packages/frontend", "packages/backend", @@ -48,24 +48,24 @@ "lodash": "4.17.21" }, "dependencies": { - "cssnano": "6.0.5", + "cssnano": "6.1.2", "execa": "8.0.1", "fast-glob": "3.3.2", "ignore-walk": "6.0.4", "js-yaml": "4.1.0", - "postcss": "8.4.35", - "tar": "6.2.0", - "terser": "5.28.1", - "typescript": "5.3.3", - "esbuild": "0.19.11", - "glob": "10.3.10" + "postcss": "8.4.38", + "tar": "6.2.1", + "terser": "5.30.3", + "typescript": "5.4.5", + "esbuild": "0.20.2", + "glob": "10.3.12" }, "devDependencies": { - "@types/node": "^20.11.28", - "@typescript-eslint/eslint-plugin": "7.1.0", - "@typescript-eslint/parser": "7.1.0", + "@types/node": "20.12.7", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "cross-env": "7.0.3", - "cypress": "13.6.6", + "cypress": "13.7.3", "eslint": "8.57.0", "ncp": "2.0.0", "start-server-and-test": "2.0.3" diff --git a/packages/backend/package.json b/packages/backend/package.json index 4393b06d1d..5d4e9b93a3 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -12,9 +12,9 @@ "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", "check:connect": "node ./scripts/check_connect.js", - "build": "swc src -d built -D", - "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc", - "watch:swc": "swc src -d built -D -w", + "build": "swc src -d built -D --strip-leading-paths", + "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", + "watch:swc": "swc src -d built -D -w --strip-leading-paths", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node ./scripts/watch.mjs", "restart": "pnpm build && pnpm start", @@ -65,40 +65,41 @@ "dependencies": { "@aws-sdk/client-s3": "3.412.0", "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.14.2", - "@bull-board/fastify": "5.14.2", - "@bull-board/ui": "5.14.2", - "@discordapp/twemoji": "15.0.2", + "@bull-board/api": "5.17.0", + "@bull-board/fastify": "5.17.0", + "@bull-board/ui": "5.17.0", + "@discordapp/twemoji": "15.0.3", "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.3.1", - "@fastify/cors": "8.5.0", - "@fastify/express": "2.3.0", - "@fastify/http-proxy": "9.3.0", - "@fastify/multipart": "8.1.0", - "@fastify/static": "6.12.0", - "@fastify/view": "8.2.0", + "@fastify/cors": "9.0.1", + "@fastify/express": "3.0.0", + "@fastify/http-proxy": "9.5.0", + "@fastify/multipart": "8.2.0", + "@fastify/static": "7.0.3", + "@fastify/view": "9.1.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", - "@nestjs/common": "10.3.3", - "@nestjs/core": "10.3.3", - "@nestjs/testing": "10.3.3", + "@napi-rs/canvas": "^0.1.52", + "@nestjs/common": "10.3.8", + "@nestjs/core": "10.3.8", + "@nestjs/testing": "10.3.8", "@peertube/http-signature": "1.7.0", - "@simplewebauthn/server": "9.0.3", + "@simplewebauthn/server": "10.0.0", "@sinonjs/fake-timers": "11.2.2", - "@smithy/node-http-handler": "2.1.10", - "@swc/cli": "0.1.63", - "@swc/core": "1.3.107", + "@smithy/node-http-handler": "2.5.0", + "@swc/cli": "0.3.12", + "@swc/core": "1.4.17", "@transfem-org/sfm-js": "0.24.5", - "@twemoji/parser": "15.0.0", + "@twemoji/parser": "15.1.1", "accepts": "1.3.8", - "ajv": "8.12.0", - "archiver": "6.0.1", + "ajv": "8.13.0", + "archiver": "7.0.1", "argon2": "^0.40.1", - "async-mutex": "0.4.1", + "async-mutex": "0.5.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.4.0", + "bullmq": "5.7.8", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -109,7 +110,7 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "4.25.2", + "fastify": "4.26.2", "fastify-multer": "^2.0.3", "fastify-raw-body": "4.3.0", "feed": "4.2.2", @@ -117,79 +118,78 @@ "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "glob": "10.3.10", - "got": "14.2.0", - "happy-dom": "10.0.3", + "got": "14.2.1", + "happy-dom": "14.7.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", - "http-link-header": "1.1.2", - "ioredis": "5.3.2", + "http-link-header": "1.1.3", + "ioredis": "5.4.1", "ip-cidr": "3.1.0", - "ipaddr.js": "2.1.0", + "ipaddr.js": "2.2.0", "is-svg": "5.0.0", "js-yaml": "4.1.0", - "jsdom": "23.2.0", + "jsdom": "24.0.0", "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", "megalodon": "workspace:*", - "meilisearch": "0.37.0", + "meilisearch": "0.38.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.0.6", + "nanoid": "5.0.7", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.10", + "nodemailer": "6.9.13", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.2.2", + "otpauth": "9.2.3", "parse5": "7.1.2", - "pg": "8.11.3", + "pg": "8.11.5", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "pug": "3.0.2", "punycode": "2.3.1", - "pureimage": "0.3.17", "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.9", + "re2": "1.20.10", "redis-lock": "0.1.4", - "reflect-metadata": "0.2.1", + "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", - "sanitize-html": "2.12.1", + "sanitize-html": "2.13.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.2", + "sharp": "0.33.3", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.22.0", + "systeminformation": "5.22.7", "tinycolor2": "1.6.0", "tmp": "0.2.3", "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", - "typescript": "5.3.3", + "typescript": "5.4.5", "ulid": "2.3.0", "uuid": "^9.0.1", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.16.0", + "ws": "8.17.0", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", "@misskey-dev/eslint-plugin": "1.0.0", - "@nestjs/platform-express": "10.3.3", - "@simplewebauthn/types": "9.0.1", - "@swc/jest": "0.2.31", + "@nestjs/platform-express": "10.3.8", + "@simplewebauthn/types": "10.0.0", + "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", @@ -199,20 +199,20 @@ "@types/fluent-ffmpeg": "2.1.24", "@types/htmlescape": "^1.1.3", "@types/http-link-header": "1.0.5", - "@types/jest": "29.5.11", + "@types/jest": "29.5.12", "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.6", "@types/jsonld": "1.5.13", - "@types/jsrsasign": "10.5.12", + "@types/jsrsasign": "10.5.14", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.11.22", + "@types/node": "20.12.7", "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.14", + "@types/nodemailer": "6.4.15", "@types/oauth": "0.9.4", - "@types/oauth2orize": "1.11.3", + "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.2", + "@types/pg": "8.11.5", "@types/pug": "2.0.10", "@types/punycode": "2.1.4", "@types/qrcode": "1.5.5", @@ -229,8 +229,8 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.3", "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.1.0", - "@typescript-eslint/parser": "7.1.0", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "aws-sdk-client-mock": "3.0.1", "cross-env": "7.0.3", "eslint": "8.57.0", diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 48a2a2b80c..e68cb86af0 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -127,7 +127,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js'; import { ApRendererService } from './activitypub/ApRendererService.js'; import { ApRequestService } from './activitypub/ApRequestService.js'; import { ApResolverService } from './activitypub/ApResolverService.js'; -import { LdSignatureService } from './activitypub/LdSignatureService.js'; +import { JsonLdService } from './activitypub/JsonLdService.js'; import { RemoteLoggerService } from './RemoteLoggerService.js'; import { RemoteUserResolveService } from './RemoteUserResolveService.js'; import { WebfingerService } from './WebfingerService.js'; @@ -266,7 +266,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService }; const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService }; const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService }; -const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService }; +const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService }; const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService }; const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService }; const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService }; @@ -406,7 +406,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApRendererService, ApRequestService, ApResolverService, - LdSignatureService, + JsonLdService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -542,7 +542,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApRendererService, $ApRequestService, $ApResolverService, - $LdSignatureService, + $JsonLdService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, @@ -678,7 +678,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApRendererService, ApRequestService, ApResolverService, - LdSignatureService, + JsonLdService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -813,7 +813,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApRendererService, $ApRequestService, $ApResolverService, - $LdSignatureService, + $JsonLdService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index acc1c604e6..2fbba4d6c5 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -22,7 +22,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Config } from '@/config.js'; import { DriveService } from './DriveService.js'; -const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; +const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; @Injectable() export class CustomEmojiService implements OnApplicationShutdown { diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 79e810ce4e..461cb2b6a9 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -6,7 +6,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window } from 'happy-dom'; +import { Window, XMLSerializer } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; @@ -247,6 +247,8 @@ export class MfmService { const doc = window.document; + const body = doc.createElement('p'); + function appendChildren(children: mfm.MfmNode[], targetElement: any): void { if (children) { for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); @@ -457,9 +459,9 @@ export class MfmService { }, }; - appendChildren(nodes, doc.body); + appendChildren(nodes, body); - return `

${doc.body.innerHTML}

`; + return new XMLSerializer().serializeToString(body); } // the toMastoHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index 42fbed2110..ec9f4484a4 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -10,7 +10,7 @@ import { generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server'; -import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers'; +import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; import { DI } from '@/di-symbols.js'; import type { UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -49,7 +49,7 @@ export class WebAuthnService { const instance = await this.metaService.fetch(); return { origin: this.config.url, - rpId: this.config.host, + rpId: this.config.hostname, rpName: instance.name ?? this.config.host, rpIcon: instance.iconUrl ?? undefined, }; @@ -65,13 +65,12 @@ export class WebAuthnService { const registrationOptions = await generateRegistrationOptions({ rpName: relyingParty.rpName, rpID: relyingParty.rpId, - userID: userId, + userID: isoUint8Array.fromUTF8String(userId), userName: userName, userDisplayName: userDisplayName, attestationType: 'indirect', - excludeCredentials: keys.map(key => ({ - id: Buffer.from(key.id, 'base64url'), - type: 'public-key', + excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ + id: key.id, transports: key.transports ?? undefined, })), authenticatorSelection: { @@ -87,7 +86,7 @@ export class WebAuthnService { @bindThis public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{ - credentialID: Uint8Array; + credentialID: string; credentialPublicKey: Uint8Array; attestationObject: Uint8Array; fmt: AttestationFormat; @@ -144,6 +143,7 @@ export class WebAuthnService { @bindThis public async initiateAuthentication(userId: MiUser['id']): Promise { + const relyingParty = await this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -153,9 +153,9 @@ export class WebAuthnService { } const authenticationOptions = await generateAuthenticationOptions({ - allowCredentials: keys.map(key => ({ - id: Buffer.from(key.id, 'base64url'), - type: 'public-key', + rpID: relyingParty.rpId, + allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ + id: key.id, transports: key.transports ?? undefined, })), userVerification: 'preferred', @@ -219,7 +219,7 @@ export class WebAuthnService { expectedOrigin: relyingParty.origin, expectedRPID: relyingParty.rpId, authenticator: { - credentialID: Buffer.from(key.id, 'base64url'), + credentialID: key.id, credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), counter: key.counter, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 38175b2059..90784fdc1d 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -29,7 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { MetaService } from '../MetaService.js'; -import { LdSignatureService } from './LdSignatureService.js'; +import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @@ -61,7 +61,7 @@ export class ApRendererService { private customEmojiService: CustomEmojiService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private ldSignatureService: LdSignatureService, + private jsonLdService: JsonLdService, private userKeypairService: UserKeypairService, private apMfmService: ApMfmService, private mfmService: MfmService, @@ -173,6 +173,7 @@ export class ApRendererService { url: this.driveFileEntityService.getPublicUrl(file), name: file.comment, summary: file.comment, + sensitive: file.isSensitive, }; } @@ -794,9 +795,9 @@ export class ApRendererService { public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const ldSignature = this.ldSignatureService.use(); - ldSignature.debug = false; - activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); + const jsonLd = this.jsonLdService.use(); + jsonLd.debug = false; + activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); return activity; } diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts similarity index 86% rename from packages/backend/src/core/activitypub/LdSignatureService.ts rename to packages/backend/src/core/activitypub/JsonLdService.ts index 2ec6dc4585..100d4fa19f 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -7,14 +7,14 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -import { CONTEXT, CONTEXTS } from './misc/contexts.js'; +import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; import type { JsonLdDocument } from 'jsonld'; -import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; +import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js'; -// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 +// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017 -class LdSignature { +class JsonLd { public debug = false; public preLoad = true; public loderTimeout = 5000; @@ -101,8 +101,6 @@ class LdSignature { @bindThis public async normalize(data: JsonLdDocument): Promise { const customLoader = this.getLoader(); - // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically - // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 return (await import('jsonld')).default.normalize(data, { documentLoader: customLoader, }); @@ -114,11 +112,11 @@ class LdSignature { if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { - if (url in CONTEXTS) { + if (url in PRELOADED_CONTEXTS) { if (this.debug) console.debug(`HIT: ${url}`); return { contextUrl: undefined, - document: CONTEXTS[url], + document: PRELOADED_CONTEXTS[url], documentUrl: url, }; } @@ -135,7 +133,7 @@ class LdSignature { } @bindThis - private async fetchDocument(url: string): Promise { + private async fetchDocument(url: string): Promise { const json = await this.httpRequestService.send( url, { @@ -156,7 +154,7 @@ class LdSignature { } }); - return json as JsonLd; + return json as JsonLdObject; } @bindThis @@ -168,14 +166,14 @@ class LdSignature { } @Injectable() -export class LdSignatureService { +export class JsonLdService { constructor( private httpRequestService: HttpRequestService, ) { } @bindThis - public use(): LdSignature { - return new LdSignature(this.httpRequestService); + public use(): JsonLd { + return new JsonLd(this.httpRequestService); } } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 4ff114bbf5..815b20b910 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -570,7 +570,7 @@ const extension_context_definition = { export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]; -export const CONTEXTS: Record = { +export const PRELOADED_CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, 'https://www.w3.org/ns/activitystreams': activitystreams, diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 1017a5018a..3d98c5b764 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -18,7 +18,7 @@ import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; -import type { IObject } from '../type.js'; +import { isDocument, type IObject } from '../type.js'; @Injectable() export class ApImageService { @@ -41,7 +41,7 @@ export class ApImageService { * Imageを作成します。 */ @bindThis - public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { + public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -49,16 +49,18 @@ export class ApImageService { const image = await this.apResolverService.createResolver().resolve(value); + if (!isDocument(image)) return null; + if (image.url == null) { - throw new Error('invalid image: url not provided'); + return null; } if (typeof image.url !== 'string') { - throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2)); + return null; } if (!checkHttps(image.url)) { - throw new Error('invalid image: unexpected schema of url: ' + image.url); + return null; } this.logger.info(`Creating the Image: ${image.url}`); @@ -94,12 +96,11 @@ export class ApImageService { /** * Imageを解決します。 * - * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ - * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + * ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { - // TODO + public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { + // TODO: Misskeyに対象のImageが登録されていればそれを返す // リモートサーバーからフェッチしてきて登録 return await this.createImage(actor, value); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2658d61cc3..e70957eb61 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -4,7 +4,6 @@ */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js'; @@ -214,15 +213,13 @@ export class ApNoteService { } // 添付ファイル - // TODO: attachmentは必ずしもImageではない - // TODO: attachmentは必ずしも配列ではない - const limit = promiseLimit(2); - const files = (await Promise.all(toArray(note.attachment).map(attach => ( - limit(() => this.apImageService.resolveImage(actor, { - ...attach, - sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする - })) - )))); + const files: MiDriveFile[] = []; + + for (const attach of toArray(note.attachment)) { + attach.sensitive ??= note.sensitive; + const file = await this.apImageService.resolveImage(actor, attach); + if (file) files.push(file); + } // リプライ const reply: MiNote | null = note.inReplyTo diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 716515840c..cf2c3f185e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -25,6 +25,7 @@ export interface IObject { endTime?: Date; icon?: any; image?: any; + mediaType?: string; url?: ApObject | string; href?: string; tag?: IObject | IObject[]; @@ -246,14 +247,14 @@ export interface IKey extends IObject { } export interface IApDocument extends IObject { - type: 'Document'; - name: string | null; - mediaType: string; + type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; } -export interface IApImage extends IObject { +export const isDocument = (object: IObject): object is IApDocument => + ['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object)); + +export interface IApImage extends IApDocument { type: 'Image'; - name: string | null; } export interface ICreate extends IActivity { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index aa32da82f4..9e234318b5 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -257,20 +257,41 @@ export class UserEntityService implements OnModuleInit { ] = await Promise.all([ this.followingsRepository.findBy({ followerId: me }) .then(f => new Map(f.map(it => [it.followeeId, it]))), - this.followingsRepository.findBy({ followeeId: me }) - .then(it => it.map(it => it.followerId)), - this.followRequestsRepository.findBy({ followerId: me }) - .then(it => it.map(it => it.followeeId)), - this.followRequestsRepository.findBy({ followeeId: me }) - .then(it => it.map(it => it.followerId)), - this.blockingsRepository.findBy({ blockerId: me }) - .then(it => it.map(it => it.blockeeId)), - this.blockingsRepository.findBy({ blockeeId: me }) - .then(it => it.map(it => it.blockerId)), - this.mutingsRepository.findBy({ muterId: me }) - .then(it => it.map(it => it.muteeId)), - this.renoteMutingsRepository.findBy({ muterId: me }) - .then(it => it.map(it => it.muteeId)), + this.followingsRepository.createQueryBuilder('f') + .select('f.followerId') + .where('f.followeeId = :me', { me }) + .getRawMany<{ f_followerId: string }>() + .then(it => it.map(it => it.f_followerId)), + this.followRequestsRepository.createQueryBuilder('f') + .select('f.followeeId') + .where('f.followerId = :me', { me }) + .getRawMany<{ f_followeeId: string }>() + .then(it => it.map(it => it.f_followeeId)), + this.followRequestsRepository.createQueryBuilder('f') + .select('f.followerId') + .where('f.followeeId = :me', { me }) + .getRawMany<{ f_followerId: string }>() + .then(it => it.map(it => it.f_followerId)), + this.blockingsRepository.createQueryBuilder('b') + .select('b.blockeeId') + .where('b.blockerId = :me', { me }) + .getRawMany<{ b_blockeeId: string }>() + .then(it => it.map(it => it.b_blockeeId)), + this.blockingsRepository.createQueryBuilder('b') + .select('b.blockerId') + .where('b.blockeeId = :me', { me }) + .getRawMany<{ b_blockerId: string }>() + .then(it => it.map(it => it.b_blockerId)), + this.mutingsRepository.createQueryBuilder('m') + .select('m.muteeId') + .where('m.muterId = :me', { me }) + .getRawMany<{ m_muteeId: string }>() + .then(it => it.map(it => it.m_muteeId)), + this.renoteMutingsRepository.createQueryBuilder('m') + .select('m.muteeId') + .where('m.muterId = :me', { me }) + .getRawMany<{ m_muteeId: string }>() + .then(it => it.map(it => it.m_muteeId)), ]); return new Map( @@ -682,18 +703,17 @@ export class UserEntityService implements OnModuleInit { } const _userIds = _users.map(u => u.id); - // -- 特に前提条件のない値群を取得 - - const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) - .then(profiles => new Map(profiles.map(p => [p.userId, p]))); - // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得 + let profilesMap: Map = new Map(); let userRelations: Map = new Map(); let userMemos: Map = new Map(); let pinNotes: Map = new Map(); if (options?.schema !== 'UserLite') { + profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) + .then(profiles => new Map(profiles.map(p => [p.userId, p]))); + const meId = me ? me.id : null; if (meId) { userMemos = await this.userMemosRepository.findBy({ userId: meId }) diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index 62a8ab8ace..342e0f8602 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -8,9 +8,8 @@ * https://en.wikipedia.org/wiki/Identicon */ -import * as p from 'pureimage'; +import { createCanvas } from '@napi-rs/canvas'; import gen from 'random-seed'; -import type { WriteStream } from 'node:fs'; const size = 128; // px const n = 5; // resolution @@ -45,9 +44,9 @@ const sideN = Math.floor(n / 2); /** * Generate buffer of an identicon by seed */ -export function genIdenticon(seed: string, stream: WriteStream): Promise { +export async function genIdenticon(seed: string): Promise { const rand = gen.create(seed); - const canvas = p.make(size, size, undefined); + const canvas = createCanvas(size, size); const ctx = canvas.getContext('2d'); const bgColors = colors[rand(colors.length)]; @@ -101,5 +100,5 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise { } } - return p.encodePNGToStream(canvas, stream); + return await canvas.encode('png'); } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 2b5b7c5619..ce32a482fd 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -22,7 +22,7 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; +import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; @@ -39,7 +39,7 @@ export class InboxProcessorService { private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, - private ldSignatureService: LdSignatureService, + private jsonLdService: JsonLdService, private apPersonService: ApPersonService, private apDbResolverService: ApDbResolverService, private instanceChart: InstanceChart, @@ -111,7 +111,7 @@ export class InboxProcessorService { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { let renewKeyFailed = true; - + if (!httpSignatureValidated) { authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); @@ -122,20 +122,21 @@ export class InboxProcessorService { } // 一致しなくても、でもLD-Signatureがありそうならそっちも見る - if (activity.signature && renewKeyFailed) { - if (activity.signature.type !== 'RsaSignature2017') { - throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); + const ldSignature = activity.signature; + if (ldSignature) { + if (ldSignature.type !== 'RsaSignature2017') { + throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`); } - // activity.signature.creator: https://example.oom/users/user#main-key + // ldSignature.creator: https://example.oom/users/user#main-key // みたいになっててUserを引っ張れば公開キーも入ることを期待する - if (activity.signature.creator) { - const candicate = activity.signature.creator.replace(/#.*/, ''); + if (ldSignature.creator) { + const candicate = ldSignature.creator.replace(/#.*/, ''); await this.apPersonService.resolvePerson(candicate).catch(() => null); } // keyIdからLD-Signatureのユーザーを取得 - authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); + authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator); if (authUser == null) { throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } @@ -144,9 +145,10 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); } + const jsonLd = this.jsonLdService.use(); + // LD-Signature検証 - const ldSignature = this.ldSignatureService.use(); - const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); + const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); if (!verified) { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } @@ -154,7 +156,7 @@ export class InboxProcessorService { // アクティビティを正規化 delete activity.signature; try { - activity = await ldSignature.compact(activity) as IActivity; + activity = await jsonLd.compact(activity) as IActivity; } catch (e) { throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`); } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 6adf8d9f9f..2cf826deac 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -18,7 +18,6 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { genIdenticon } from '@/misc/gen-identicon.js'; -import { createTemp } from '@/misc/create-temp.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; @@ -194,9 +193,7 @@ export class ServerService implements OnApplicationShutdown { reply.header('Cache-Control', 'public, max-age=86400'); if ((await this.metaService.fetch()).enableIdenticonGeneration) { - const [temp, cleanup] = await createTemp(); - await genIdenticon(request.params.x, fs.createWriteStream(temp)); - return fs.createReadStream(temp).on('close', () => cleanup()); + return await genIdenticon(request.params.x); } else { return reply.redirect('/static-assets/avatar.png'); } diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 2085b06365..ba48b0119e 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -20,13 +20,188 @@ export const meta = { res: { type: 'object', properties: { + image: { + type: 'object', + optional: true, + properties: { + link: { + type: 'string', + optional: true, + }, + url: { + type: 'string', + optional: false, + }, + title: { + type: 'string', + optional: true, + }, + }, + }, + paginationLinks: { + type: 'object', + optional: true, + properties: { + self: { + type: 'string', + optional: true, + }, + first: { + type: 'string', + optional: true, + }, + next: { + type: 'string', + optional: true, + }, + last: { + type: 'string', + optional: true, + }, + prev: { + type: 'string', + optional: true, + }, + }, + }, + link: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, items: { type: 'array', + optional: false, items: { type: 'object', + properties: { + link: { + type: 'string', + optional: true, + }, + guid: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, + pubDate: { + type: 'string', + optional: true, + }, + creator: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + content: { + type: 'string', + optional: true, + }, + isoDate: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + contentSnippet: { + type: 'string', + optional: true, + }, + enclosure: { + type: 'object', + optional: true, + properties: { + url: { + type: 'string', + optional: false, + }, + length: { + type: 'number', + optional: true, + }, + type: { + type: 'string', + optional: true, + }, + }, + }, + }, }, - } - } + }, + feedUrl: { + type: 'string', + optional: true, + }, + description: { + type: 'string', + optional: true, + }, + itunes: { + type: 'object', + optional: true, + additionalProperties: true, + properties: { + image: { + type: 'string', + optional: true, + }, + owner: { + type: 'object', + optional: true, + properties: { + name: { + type: 'string', + optional: true, + }, + email: { + type: 'string', + optional: true, + }, + }, + }, + author: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + explicit: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + keywords: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + }, + }, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 74ee90b3dd..a4e61a0e8f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -97,10 +97,10 @@ export default class extends Endpoint { } const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential); + const keyId = keyInfo.credentialID; - const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url'); await this.userSecurityKeysRepository.insert({ - id: credentialId, + id: keyId, userId: me.id, name: ps.name, publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'), @@ -117,7 +117,7 @@ export default class extends Endpoint { })); return { - id: credentialId, + id: keyId, name: ps.name, }; }); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index cb41c4f338..84c10370f6 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -202,9 +202,18 @@ export class ClientServerService { // Authenticate fastify.addHook('onRequest', async (request, reply) => { + if (request.routeOptions.url == null) { + reply.code(404).send('Not found'); + return; + } + // %71ueueとかでリクエストされたら困るため const url = decodeURI(request.routeOptions.url); if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { + if (!url.startsWith(bullBoardPath + '/static/')) { + reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); + } + const token = request.cookies.token; if (token == null) { reply.code(401).send('Login required'); diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 08bb08ffe7..03c50eca8a 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -3,7 +3,7 @@ extends ./base block vars - const user = page.user; - const title = page.title; - - const url = `${config.url}/@${user.username}/${page.name}`; + - const url = `${config.url}/@${user.username}/pages/${page.name}`; block title = `${title} | ${instanceName}` diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 85242f2bc7..8d5683329f 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -39,6 +39,12 @@ describe('MfmService', () => { const output = '

foo bar

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); + + test('escape', () => { + const input = '```\n

Hello, world!

\n```'; + const output = '

<p>Hello, world!</p>

'; + assert.equal(mfmService.toHtml(mfm.parse(input)), output); + }); }); describe('fromHtml', () => { diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index bd335bb2fe..2e80b64a9f 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -13,11 +13,13 @@ import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; +import { CONTEXT } from '@/core/activitypub/misc/contexts.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js'; +import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; import { MiMeta, MiNote } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; @@ -88,6 +90,7 @@ describe('ActivityPub', () => { let noteService: ApNoteService; let personService: ApPersonService; let rendererService: ApRendererService; + let jsonLdService: JsonLdService; let resolver: MockResolver; const metaInitial = { @@ -128,6 +131,7 @@ describe('ActivityPub', () => { personService = app.get(ApPersonService); rendererService = app.get(ApRendererService); imageService = app.get(ApImageService); + jsonLdService = app.get(JsonLdService); resolver = new MockResolver(await app.resolve(LoggerService)); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error @@ -295,7 +299,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(!driveFile.isLink); + assert.ok(driveFile && !driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -308,7 +312,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(!sensitiveDriveFile.isLink); + assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink); }); test('cacheRemoteFiles=false disables caching', async () => { @@ -324,7 +328,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile.isLink); + assert.ok(driveFile && driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -337,7 +341,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile.isLink); + assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { @@ -353,7 +357,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(!driveFile.isLink); + assert.ok(driveFile && !driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -366,7 +370,57 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile.isLink); + assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); + }); + + test('Link is not an attachment files', async () => { + const linkObject: IObject = { + type: 'Link', + href: 'https://example.com/', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + linkObject, + ); + assert.strictEqual(driveFile, null); + }); + }); + + describe('JSON-LD', () =>{ + test('Compaction', async () => { + const jsonLd = jsonLdService.use(); + + const object = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote', + unknown: 'https://example.org/ns#unknown', + undefined: null, + }, + ], + id: 'https://example.com/notes/42', + type: 'Note', + attributedTo: 'https://example.com/users/1', + to: ['https://www.w3.org/ns/activitystreams#Public'], + content: 'test test foo', + _misskey_quote: 'https://example.com/notes/1', + unknown: 'test test bar', + undefined: 'test test baz', + }; + const compacted = await jsonLd.compact(object); + + assert.deepStrictEqual(compacted, { + '@context': CONTEXT, + id: 'https://example.com/notes/42', + type: 'Note', + attributedTo: 'https://example.com/users/1', + to: 'as:Public', + content: 'test test foo', + _misskey_quote: 'https://example.com/notes/1', + 'https://example.org/ns#unknown': 'test test bar', + // undefined: 'test test baz', + }); }); }); }); diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index de28d343d1..a2325e69c6 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -27,7 +27,7 @@ export function galleryPost(isSensitive = false) { id: 'somepostid', createdAt: '2016-12-28T22:49:51.000Z', updatedAt: '2016-12-28T22:49:51.000Z', - userid: 'someuserid', + userId: 'someuserid', user: userDetailed(), title: 'Some post title', description: 'Some post description', @@ -75,9 +75,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', avatarDecorations: [], - emojis: [], + emojis: {}, bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', - bannerColor: '#000000', bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', @@ -119,11 +118,16 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi publicReactions: false, securityKeys: false, twoFactorEnabled: false, + usePasswordLessLogin: false, twoFactorBackupCodesStock: 'none', updatedAt: null, + lastFetchedAt: null, uri: null, url: null, + movedTo: null, + alsoKnownAs: null, notify: 'none', + memo: null }; } diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 1e925aede6..d74c83a500 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -82,23 +82,16 @@ function h( return Object.assign(props || {}, { type }) as T; } -declare global { - namespace JSX { - type Element = estree.Node; - type ElementClass = never; - type ElementAttributesProperty = never; - type ElementChildrenAttribute = never; - type IntrinsicAttributes = never; - type IntrinsicClassAttributes = never; - type IntrinsicElements = { - [T in keyof typeof generator as ToKebab>>]: { - [K in keyof Omit< - Parameters<(typeof generator)[T]>[0], - 'type' - >]?: Parameters<(typeof generator)[T]>[0][K]; - }; +declare namespace h.JSX { + type Element = estree.Node; + type IntrinsicElements = { + [T in keyof typeof generator as ToKebab>>]: { + [K in keyof Omit< + Parameters<(typeof generator)[T]>[0], + 'type' + >]?: Parameters<(typeof generator)[T]>[0][K]; }; - } + }; } function toStories(component: string): Promise { @@ -388,6 +381,7 @@ function toStories(component: string): Promise { '/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' + '/* eslint-disable import/no-default-export */\n' + '/* eslint-disable import/no-duplicates */\n' + + '/* eslint-disable import/order */\n' + generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 0a87488573..d3822942cd 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -34,7 +34,7 @@ const config = { disableTelemetry: true, }, async viteFinal(config) { - const replacePluginForIsChromatic = config.plugins?.findIndex((plugin) => plugin && (plugin as Partial)?.name === 'replace') ?? -1; + const replacePluginForIsChromatic = config.plugins?.findIndex((plugin: Plugin) => plugin && plugin.name === 'replace') ?? -1; if (~replacePluginForIsChromatic) { config.plugins?.splice(replacePluginForIsChromatic, 1); } diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index 817b0125e7..29cb112ccb 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -6,7 +6,8 @@ import { type SharedOptions, http, HttpResponse } from 'msw'; export const onUnhandledRequest = ((req, print) => { - if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) { + const url = new URL(req.url); + if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) { return } print.warning() diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index e50c488243..4722fe7f5f 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +