diff --git a/.github/unused/test-backend.yml b/.github/unused/test-backend.yml index 067d31619..3d640b210 100644 --- a/.github/unused/test-backend.yml +++ b/.github/unused/test-backend.yml @@ -57,7 +57,7 @@ jobs: - name: Install FFmpeg uses: FedericoCarboni/setup-ffmpeg@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -116,7 +116,7 @@ jobs: with: run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 1b3eabc3c..eda561f91 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -26,7 +26,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb011f483..48493f2c0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,7 +29,7 @@ jobs: - uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4.2.0 + - uses: actions/setup-node@v4.3.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -54,7 +54,7 @@ jobs: - uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4.2.0 + - uses: actions/setup-node@v4.3.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -78,7 +78,7 @@ jobs: - uses: pnpm/action-setup@v4 with: run_install: false - - uses: actions/setup-node@v4.2.0 + - uses: actions/setup-node@v4.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 305b794cd..9a0e4a899 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -37,7 +37,7 @@ jobs: with: run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 2a70f0b0f..460a9461f 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -36,7 +36,7 @@ jobs: run_install: false - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 7fd6ecd0a..ec2e831d9 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -27,7 +27,7 @@ jobs: with: run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index 44198496f..486303f22 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -28,7 +28,7 @@ jobs: with: run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/Dockerfile b/Dockerfile index 3d91b9f8c..73f7be639 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=22 - # build assets & compile TypeScript -FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder +FROM --platform=$BUILDPLATFORM node:22 AS native-builder RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ @@ -17,7 +15,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ WORKDIR /misskey COPY --link pnpm-lock.yaml ./ -RUN npm install -g pnpm +RUN npm install -g pnpm@10 RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm fetch --ignore-scripts @@ -30,7 +28,7 @@ COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] -RUN pnpm i --frozen-lockfile --aggregate-output --offline \ +RUN pnpm i --frozen-lockfile --aggregate-output --prefer-offline \ && pnpm rebuild -r COPY --link . ./ @@ -39,7 +37,7 @@ RUN NODE_ENV=production pnpm build # build native dependencies for target platform -FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder +FROM --platform=$TARGETPLATFORM node:22 AS target-builder RUN apt-get update \ && apt-get install -yqq --no-install-recommends \ @@ -48,7 +46,7 @@ RUN apt-get update \ WORKDIR /misskey COPY --link pnpm-lock.yaml ./ -RUN npm install -g pnpm +RUN npm install -g pnpm@10 RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm fetch --ignore-scripts @@ -59,10 +57,10 @@ COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] -RUN pnpm i --frozen-lockfile --aggregate-output --offline \ +RUN pnpm i --frozen-lockfile --aggregate-output --prefer-offline \ && pnpm rebuild -r -FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner +FROM --platform=$TARGETPLATFORM node:22-slim AS runner ARG UID="991" ARG GID="991" @@ -81,7 +79,7 @@ RUN apt-get update \ WORKDIR /misskey COPY --chown=misskey:misskey pnpm-lock.yaml ./ -RUN npm install -g pnpm +RUN npm install -g pnpm@10 COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules diff --git a/locales/en-US.yml b/locales/en-US.yml index 96f6325f6..12671580c 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1354,6 +1354,10 @@ scheduled: "Scheduled" unschedule: "Unschedule" setScheduledTime: "Set scheduled time" willBePostedAt: "Note will be posted at {x}" +sensitiveByModerator: "This file has been marked as sensitive by the administrator.\nFor more information, please check the [NSFW Guidelines](https://go.misskey.io/media-guideline)." +thisInfoIsNotVisibleOtherUser: "This information is not visible to other users." +flushItAway: "Let it go" +deleteNotWash: "Deleting this won't make everything let bygones be bygones, but..." _bubbleGame: howToPlay: "How to play" @@ -2544,6 +2548,7 @@ _notification: renotedBySomeUsers: "Renote from {n} users" followedBySomeUsers: "Followed by {n} users" flushNotification: "Clear notifications" + sensitiveFlagAssigned: "Your file has been marked as sensitive" _types: all: "All" note: "New notes" diff --git a/locales/index.d.ts b/locales/index.d.ts index 34c843b35..793a3d103 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5564,6 +5564,23 @@ export interface Locale extends ILocale { * {x}に投稿されます */ "willBePostedAt": ParameterizedString<"x">; + /** + * 管理者によって、ドライブのファイルがセンシティブとして設定されました。 + * 詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。 + */ + "sensitiveByModerator": string; + /** + * この情報は他のユーザーには公開されません。 + */ + "thisInfoIsNotVisibleOtherUser": string; + /** + * 水に流す + */ + "flushItAway": string; + /** + * 削除をしても全てが水に流れるわけではありませんが… + */ + "deleteNotWash": string; "_bubbleGame": { /** * 遊び方 @@ -10089,6 +10106,10 @@ export interface Locale extends ILocale { * 通知の履歴をリセットする */ "flushNotification": string; + /** + * ファイルがセンシティブとして設定されました + */ + "sensitiveFlagAssigned": string; "_types": { /** * すべて diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ef1293924..9d8bb3e73 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1384,6 +1384,10 @@ scheduled: "予約済み" unschedule: "予約を解除" setScheduledTime: "予約日時を設定" willBePostedAt: "{x}に投稿されます" +sensitiveByModerator: "管理者によって、ドライブのファイルがセンシティブとして設定されました。\n詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。" +thisInfoIsNotVisibleOtherUser: "この情報は他のユーザーには公開されません。" +flushItAway: "水に流す" +deleteNotWash: "削除をしても全てが水に流れるわけではありませんが…" _bubbleGame: howToPlay: "遊び方" @@ -2651,6 +2655,7 @@ _notification: renotedBySomeUsers: "{n}人がリノートしました" followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" + sensitiveFlagAssigned: "ファイルがセンシティブとして設定されました" _types: all: "すべて" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 27416bcce..2f70a3a73 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1374,6 +1374,10 @@ scheduled: "예약됨" unschedule: "예약 취소" setScheduledTime: "예약 시간 설정" willBePostedAt: "{x}에 게시됩니다" +sensitiveByModerator: "관리자에 의해 드라이브의 파일이 열람주의로 설정되었습니다.\n자세한 내용은 [NSFW 가이드라인](https://go.misskey.io/media-guideline)을 확인해 주세요." +thisInfoIsNotVisibleOtherUser: "이 정보는 다른 사용자에게 공개되지 않습니다." +flushItAway: "묻어두기" +deleteNotWash: "삭제해도 모든 것이 없었던 일이 되는 것은 아닙니다만..." _bubbleGame: howToPlay: "설명" @@ -2574,6 +2578,7 @@ _notification: renotedBySomeUsers: "{n}명이 리노트함" followedBySomeUsers: "{n}명이 팔로우함" flushNotification: "알림 이력을 초기화" + sensitiveFlagAssigned: "파일이 열람주의로 설정되었습니다" _types: all: "전부" note: "유저의 새 글" diff --git a/package.json b/package.json index 87a666676..73ec3ad4b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://git.psec.dev/oscar-surf/misskey.git" }, - "packageManager": "pnpm@9.15.4", + "packageManager": "pnpm@10.7.0", "workspaces": [ "packages/frontend", "packages/backend", @@ -46,38 +46,42 @@ "cleanall": "pnpm clean-all" }, "resolutions": { + "@aiscript-dev/aiscript-languageserver": "github:aiscript-dev/aiscript-languageserver#0.1.6", "@tensorflow/tfjs-core": "4.22.0", - "axios": "1.7.9", + "axios": "1.8.4", "chokidar": "4.0.3", "cookie": "1.0.2", "cookie-signature": "1.2.2", "debug": "4.4.0", - "esbuild": "0.24.2", + "esbuild": "0.25.2", "jpeg-js": "0.4.4", "lodash": "4.17.21", "sharp": "0.33.5", - "tough-cookie": "5.1.0", + "tough-cookie": "5.1.2", "web-streams-polyfill": "4.1.0" }, "dependencies": { "cssnano": "7.0.6", "execa": "9.5.2", "js-yaml": "4.1.0", - "postcss": "8.5.1", - "terser": "5.37.0", - "typescript": "5.7.3" + "postcss": "8.5.3", + "terser": "5.39.0", + "typescript": "5.8.2" }, "devDependencies": { - "@types/node": "22.10.7", + "@types/node": "22.13.14", "@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/parser": "7.10.0", "cross-env": "7.0.3", - "cypress": "13.17.0", + "cypress": "14.2.1", "eslint": "8.57.1", "ncp": "2.0.0", - "start-server-and-test": "2.0.10" + "start-server-and-test": "2.0.11" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" + }, + "pnpm": { + "neverBuiltDependencies": [] } } diff --git a/packages/backend/migration/1739335129758-sensitiveFlag.js b/packages/backend/migration/1739335129758-sensitiveFlag.js new file mode 100644 index 000000000..b3ca6df6c --- /dev/null +++ b/packages/backend/migration/1739335129758-sensitiveFlag.js @@ -0,0 +1,13 @@ +export class SensitiveFlag1739335129758 { + name = 'SensitiveFlag1739335129758' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ADD "isSensitiveByModerator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_e779d1afdfa44dc3d64213cd2e" ON "drive_file" ("isSensitiveByModerator") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_e779d1afdfa44dc3d64213cd2e"`); + await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "isSensitiveByModerator"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 2dcfff2fa..5e6761b79 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -33,16 +33,16 @@ "generate-api-json": "pnpm build && node ./scripts/generate_api_json.js" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.12", - "@swc/core-darwin-x64": "1.10.12", - "@swc/core-linux-arm-gnueabihf": "1.10.12", - "@swc/core-linux-arm64-gnu": "1.10.12", - "@swc/core-linux-arm64-musl": "1.10.12", - "@swc/core-linux-x64-gnu": "1.10.12", - "@swc/core-linux-x64-musl": "1.10.12", - "@swc/core-win32-arm64-msvc": "1.10.12", - "@swc/core-win32-ia32-msvc": "1.10.12", - "@swc/core-win32-x64-msvc": "1.10.12", + "@swc/core-darwin-arm64": "1.11.13", + "@swc/core-darwin-x64": "1.11.13", + "@swc/core-linux-arm-gnueabihf": "1.11.13", + "@swc/core-linux-arm64-gnu": "1.11.13", + "@swc/core-linux-arm64-musl": "1.11.13", + "@swc/core-linux-x64-gnu": "1.11.13", + "@swc/core-linux-x64-musl": "1.11.13", + "@swc/core-win32-arm64-msvc": "1.11.13", + "@swc/core-win32-ia32-msvc": "1.11.13", + "@swc/core-win32-x64-msvc": "1.11.13", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -63,94 +63,94 @@ }, "dependencies": { "@authenio/samlify-node-xmllint": "2.0.0", - "@aws-sdk/client-s3": "3.740.0", - "@aws-sdk/lib-storage": "3.740.0", - "@bull-board/api": "6.7.4", - "@bull-board/fastify": "6.7.4", - "@bull-board/ui": "6.7.4", + "@aws-sdk/client-s3": "3.777.0", + "@aws-sdk/lib-storage": "3.777.0", + "@bull-board/api": "6.7.10", + "@bull-board/fastify": "6.7.10", + "@bull-board/ui": "6.7.10", "@discordapp/twemoji": "15.1.0", - "@elastic/elasticsearch": "8.17.0", + "@elastic/elasticsearch": "8.17.1", "@fastify/accepts": "5.0.2", "@fastify/cookie": "11.0.2", - "@fastify/cors": "10.0.2", + "@fastify/cors": "11.0.1", "@fastify/express": "4.0.2", "@fastify/formbody": "8.0.2", - "@fastify/http-proxy": "11.0.1", + "@fastify/http-proxy": "11.1.2", "@fastify/multipart": "9.0.3", - "@fastify/static": "8.0.4", - "@fastify/view": "10.0.2", + "@fastify/static": "8.1.1", + "@fastify/view": "11.0.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3", - "@napi-rs/canvas": "0.1.65", - "@nestjs/common": "11.0.7", - "@nestjs/core": "11.0.7", - "@nestjs/testing": "11.0.7", + "@napi-rs/canvas": "0.1.68", + "@nestjs/common": "11.0.12", + "@nestjs/core": "11.0.12", + "@nestjs/testing": "11.0.12", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "13.1.1", "@sinonjs/fake-timers": "11.3.1", - "@smithy/node-http-handler": "4.0.2", + "@smithy/node-http-handler": "4.0.4", "@swc/cli": "0.6.0", - "@swc/core": "1.10.12", + "@swc/core": "1.11.13", "@twemoji/parser": "15.1.1", "accepts": "1.3.8", "ajv": "8.17.1", "archiver": "7.0.1", "async-mutex": "0.5.0", - "bcryptjs": "2.4.3", + "bcryptjs": "3.0.2", "blurhash": "2.0.5", - "body-parser": "1.20.3", - "bullmq": "5.39.1", + "body-parser": "2.2.0", + "bullmq": "5.45.2", "cacheable-lookup": "7.0.0", "cbor": "10.0.3", "chalk": "5.4.1", "chalk-template": "1.1.0", "chokidar": "4.0.3", "cli-highlight": "2.1.11", - "color-convert": "2.0.1", + "color-convert": "3.0.1", "content-disposition": "0.5.4", "date-fns": "4.1.0", "deep-email-validator": "0.1.21", - "fastify": "5.2.1", + "fastify": "5.2.2", "fastify-http-errors-enhanced": "6.0.1", "fastify-raw-body": "5.0.0", "feed": "4.2.2", - "file-type": "20.0.1", + "file-type": "20.4.1", "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.1", - "got": "14.4.5", + "form-data": "4.0.2", + "got": "14.4.6", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.4.2", + "ioredis": "5.6.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", - "jose": "5.9.6", + "jose": "6.0.10", "js-yaml": "4.1.0", "jsdom": "26.0.0", "json5": "2.2.3", "jsonld": "8.3.3", "jsrsasign": "11.1.0", - "meilisearch": "0.48.2", + "meilisearch": "0.49.0", "mfm-js": "0.24.0", "microformats-parser": "2.0.2", - "mime-types": "2.1.35", + "mime-types": "3.0.1", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.0.9", + "nanoid": "5.1.5", "nested-property": "4.0.0", "node-fetch": "3.3.2", "node-forge": "1.3.1", "nodemailer": "6.10.0", "nsfwjs": "4.2.0", - "oauth": "0.10.0", + "oauth": "0.10.2", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.3.6", + "otpauth": "9.4.0", "parse5": "7.2.1", - "pg": "8.13.1", + "pg": "8.14.1", "pino": "9.6.0", "pino-pretty": "13.0.0", "pkce-challenge": "4.1.0", @@ -166,10 +166,10 @@ "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", - "rxjs": "7.8.1", - "samlify": "2.8.11", - "sanitize-html": "2.14.0", - "secure-json-parse": "3.0.2", + "rxjs": "7.8.2", + "samlify": "2.9.1", + "sanitize-html": "2.15.0", + "secure-json-parse": "4.0.0", "sharp": "0.33.5", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", @@ -177,25 +177,24 @@ "systeminformation": "5.25.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.10", + "tsc-alias": "1.8.13", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.20", - "typescript": "5.7.3", - "ulid": "2.3.0", + "typeorm": "0.3.21", + "typescript": "5.8.2", + "ulid": "3.0.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.18.0", + "ws": "8.18.1", "xev": "3.0.2", "xmlbuilder": "15.1.1" }, "devDependencies": { "@jest/globals": "29.7.0", "@misskey-dev/eslint-plugin": "1.0.0", - "@nestjs/platform-express": "11.0.7", + "@nestjs/platform-express": "11.0.12", "@swc/jest": "0.2.37", "@types/accepts": "1.3.7", "@types/archiver": "6.0.3", - "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", "@types/color-convert": "2.0.4", "@types/content-disposition": "0.5.8", @@ -209,7 +208,7 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "2.1.0", - "@types/node": "22.13.0", + "@types/node": "22.13.14", "@types/node-forge": "1.3.11", "@types/nodemailer": "6.4.17", "@types/oauth": "0.9.6", @@ -223,15 +222,15 @@ "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.13.0", - "@types/semver": "7.5.8", + "@types/sanitize-html": "2.15.0", + "@types/semver": "7.7.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.4", - "@types/ws": "8.5.14", + "@types/ws": "8.18.0", "@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/parser": "7.10.0", "aws-sdk-client-mock": "4.1.0", diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index a68fc25ab..1a744f1b4 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -44,6 +44,7 @@ import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -129,6 +130,7 @@ export class DriveService { private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, + private notificationService: NotificationService, ) { const logger = this.loggerService.getLogger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -146,8 +148,10 @@ export class DriveService { */ @bindThis private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { - // thunbnail, webpublic を必要なら生成 - const alts = await this.generateAlts(path, type, !file.uri); + const fileType = type.split(';')[0]; + + // thunbnail, webpublic を必要なら生成 + const alts = await this.generateAlts(path, fileType, !file.uri); const meta = await this.metaService.fetch(); @@ -156,17 +160,17 @@ export class DriveService { let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); if (ext === '') { - if (type === 'image/jpeg') ext = '.jpg'; - if (type === 'image/png') ext = '.png'; - if (type === 'image/webp') ext = '.webp'; - if (type === 'image/avif') ext = '.avif'; - if (type === 'image/apng') ext = '.apng'; - if (type === 'image/vnd.mozilla.apng') ext = '.apng'; + if (fileType === 'image/jpeg') ext = '.jpg'; + if (fileType === 'image/png') ext = '.png'; + if (fileType === 'image/webp') ext = '.webp'; + if (fileType === 'image/avif') ext = '.avif'; + if (fileType === 'image/apng') ext = '.apng'; + if (fileType === 'image/vnd.mozilla.apng') ext = '.apng'; } // 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、 // 許可されているファイル形式でしかURLに拡張子をつけない - if (!FILE_TYPE_BROWSERSAFE.includes(type)) { + if (!FILE_TYPE_BROWSERSAFE.includes(fileType)) { ext = ''; } @@ -373,8 +377,10 @@ export class DriveService { */ @bindThis private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, ext?: string | null, filename?: string) { - if (type === 'image/apng') type = 'image/png'; - if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; + const fileType = type.split(';')[0]; + + if (fileType === 'image/apng') type = 'image/png'; + if (!FILE_TYPE_BROWSERSAFE.includes(fileType)) type = 'application/octet-stream'; const meta = await this.metaService.fetch(); @@ -660,13 +666,15 @@ export class DriveService { @bindThis public async updateFile(file: MiDriveFile, values: Partial, updater: MiUser) { const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw; + const isModerator = await this.roleService.isModerator(updater); if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) { throw new DriveService.InvalidFileNameError(); } - if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) { - throw new DriveService.CannotUnmarkSensitiveError(); + if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && !values.isSensitive) { + if (alwaysMarkNsfw) throw new DriveService.CannotUnmarkSensitiveError(); + if (file.isSensitiveByModerator && (file.userId === updater.id)) throw new DriveService.CannotUnmarkSensitiveError(); } if (values.folderId != null) { @@ -680,6 +688,10 @@ export class DriveService { } } + if (isModerator && file.userId !== updater.id) { + values.isSensitiveByModerator = values.isSensitive; + } + await this.driveFilesRepository.update(file.id, values); const fileObj = await this.driveFileEntityService.pack(file.id, updater, { self: true }); @@ -689,7 +701,7 @@ export class DriveService { this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj); } - if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) { + if (isModerator && (file.userId !== updater.id)) { if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) { const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; if (values.isSensitive) { @@ -699,6 +711,11 @@ export class DriveService { fileUserUsername: user?.username ?? null, fileUserHost: user?.host ?? null, }); + if (file.userId) { + this.notificationService.createNotification(file.userId, 'sensitiveFlagAssigned', { + fileId: file.id, + }); + } } else { this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', { fileId: file.id, diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 903cef77b..9cc821602 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -69,7 +69,8 @@ function compileQuery(q: Q): string { export class SearchService { private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local'; private meilisearchNoteIndex: Index | null = null; - private elasticsearchNoteIndex: string | null = null; + private readonly elasticsearchNoteIndex: string; + private readonly elasticsearchIdField: string; private logger: Logger; constructor( @@ -121,7 +122,9 @@ export class SearchService { }, });*/ } else if (this.elasticsearch) { - this.elasticsearchNoteIndex = `${config.elasticsearch!.index}---notes`; + this.elasticsearchNoteIndex = `${config.elasticsearch!.index}`; + this.elasticsearchIdField = `${config.host}_id`; + /* 外部からindexさせるのでこの処理は不要 this.elasticsearch.indices.exists({ index: this.elasticsearchNoteIndex, }).then((indexExists: boolean) => { @@ -175,6 +178,7 @@ export class SearchService { }).catch((error: any) => { this.logger.error('Error while checking if index exists', error); }); + */ } } @@ -213,6 +217,7 @@ export class SearchService { primaryKey: 'id', }); } else if (this.elasticsearch) { + /* 外部からindexさせるのでこの処理は不要 const body = { createdAt: createdAt.getTime(), userId: note.userId, @@ -229,6 +234,7 @@ export class SearchService { }).catch((error: any) => { this.logger.error(error); }); + */ } } @@ -239,12 +245,14 @@ export class SearchService { if (this.meilisearch) { await this.meilisearchNoteIndex?.deleteDocument(note.id); } else if (this.elasticsearch) { + /* 外部からindexさせるのでこの処理は不要 await this.elasticsearch.delete({ index: `${this.elasticsearchNoteIndex}-${this.idService.parse(note.id).date.toISOString().slice(0, 7).replace(/-/g, '')}`, id: note.id, }).catch((error) => { this.logger.error(error); }); + */ } } @@ -346,7 +354,7 @@ export class SearchService { if (opts.channelId) esFilter.bool.must.push({ term: { channelId: opts.channelId } }); if (opts.host) { if (opts.host === '.') { - esFilter.bool.must.push({ bool: { must_not: [{ exists: { field: 'userHost' } }] } }); + esFilter.bool.must.push({ term: { userHost: this.config.host } }); } else { esFilter.bool.must.push({ term: { userHost: opts.host } }); } @@ -358,8 +366,6 @@ export class SearchService { should: [ { wildcard: { 'text': { value: q } } }, { simple_query_string: { fields: ['text'], 'query': q, default_operator: 'and' } }, - { wildcard: { 'cw': { value: q } } }, - { simple_query_string: { fields: ['cw'], 'query': q, default_operator: 'and' } }, ], minimum_should_match: 1, }, @@ -368,15 +374,16 @@ export class SearchService { const res = await (this.elasticsearch.search)({ index: this.elasticsearchNoteIndex + '*' as string, - body: { - query: esFilter, - sort: [{ createdAt: { order: 'desc' } }], - }, - _source: ['id', 'createdAt'], + query: esFilter, + sort: [{ createdAt: { order: 'desc' } }], + _source: ['id', 'createdAt', this.elasticsearchIdField], size: pagination.limit, }); - const noteIds = res.hits.hits.map((hit: any) => hit._id); + const noteIds = res.hits.hits.map((hit) => { + const source = hit._source as Record; + return (source[this.elasticsearchIdField] as string) || null; + }).filter((id): id is string => id !== null); if (noteIds.length === 0) return []; const notes = await this.notesRepository.findBy({ id: In(noteIds), diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 90e13153b..eb894326a 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -210,6 +210,9 @@ export class DriveFileEntityService { md5: file.md5, size: file.size, isSensitive: file.isSensitive, + ...(opts.detail ? { + isSensitiveByModerator: file.isSensitiveByModerator, + } : {}), blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), @@ -246,6 +249,9 @@ export class DriveFileEntityService { md5: file.md5, size: file.size, isSensitive: file.isSensitive, + ...(opts.detail ? { + isSensitiveByModerator: file.isSensitiveByModerator, + } : {}), blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index c216ac818..3ac9b1846 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -183,6 +183,9 @@ export class NotificationEntityService implements OnModuleInit { header: notification.customHeader, icon: notification.customIcon, } : {}), + ...(notification.type === 'sensitiveFlagAssigned' ? { + fileId: notification.fileId, + } : {}), }); } diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts index 367a8eb56..27c67cb5d 100644 --- a/packages/backend/src/misc/FileWriterStream.ts +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -4,6 +4,7 @@ */ import * as fs from 'node:fs/promises'; +import { WritableStream } from 'node:stream/web'; import type { PathLike } from 'node:fs'; /** diff --git a/packages/backend/src/misc/gen-x509-cert-from-jwk.ts b/packages/backend/src/misc/gen-x509-cert-from-jwk.ts index 1050716a4..4eb64e705 100644 --- a/packages/backend/src/misc/gen-x509-cert-from-jwk.ts +++ b/packages/backend/src/misc/gen-x509-cert-from-jwk.ts @@ -18,13 +18,13 @@ export async function genX509CertFromJWK( cert.setIssuer(attrs); cert.publicKey = await jose .importJWK(JSON.parse(publicKey)) - .then((k) => jose.exportSPKI(k as jose.KeyLike)) + .then((k) => jose.exportSPKI(k as jose.CryptoKey)) .then((k) => forge.pki.publicKeyFromPem(k)); cert.sign( await jose .importJWK(JSON.parse(privateKey)) - .then((k) => jose.exportPKCS8(k as jose.KeyLike)) + .then((k) => jose.exportPKCS8(k as jose.CryptoKey)) .then((k) => forge.pki.privateKeyFromPem(k)), forge.md.sha256.create(), ); diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index 079e9cd9d..6973e4d9d 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -162,6 +162,12 @@ export class MiDriveFile { }) public isSensitive: boolean; + @Index() + @Column('boolean', { + default: false, + }) + public isSensitiveByModerator: boolean; + @Index() @Column('boolean', { default: false, diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 4747b51b5..ef783d211 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -93,6 +93,11 @@ export type MiNotification = { id: string; createdAt: string; draftId: MiScheduledNote['id']; +} | { + type: 'sensitiveFlagAssigned' + id: string; + fileId: string; + createdAt: string; } | { type: 'app'; id: string; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index ca88cc0e3..3cc98058a 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -42,6 +42,10 @@ export const packedDriveFileSchema = { type: 'boolean', optional: false, nullable: false, }, + isSensitiveByModerator: { + type: 'boolean', + optional: true, nullable: true, + }, blurhash: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index e68240897..160fcae42 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -309,8 +309,8 @@ export const packedNotificationSchema = { type: 'object', ref: 'NoteDraft', optional: false, nullable: false, - } - } + }, + }, }, { type: 'object', properties: { @@ -324,8 +324,8 @@ export const packedNotificationSchema = { type: 'object', ref: 'Note', optional: false, nullable: false, - } - } + }, + }, }, { type: 'object', properties: { @@ -339,8 +339,21 @@ export const packedNotificationSchema = { type: 'object', ref: 'NoteDraft', optional: false, nullable: false, - } - } + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['sensitiveFlagAssigned'], + }, + fileId: { + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index d9ba5a3a7..d2758c374 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -349,7 +349,7 @@ export class FileServerService { return reply.redirect( redirectUrl, - 301, + 302, ); } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 3d2381789..f57f18db6 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -186,7 +186,7 @@ export class ServerService implements OnApplicationShutdown { return reply.redirect( url, - 301, + 302, ); }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 42c03204a..1a03827ec 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -51,6 +51,12 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7', }, + + restrictedByModerator: { + message: 'The isSensitive specified by the administrator cannot be changed.', + code: 'RESTRICTED_BY_ADMINISTRATOR', + id: '20e6c501-e579-400d-97e4-1c7efc286f35', + }, }, res: { type: 'object', @@ -105,7 +111,11 @@ export default class extends Endpoint { // eslint- } else if (e instanceof DriveService.NoSuchFolderError) { throw new ApiError(meta.errors.noSuchFolder); } else if (e instanceof DriveService.CannotUnmarkSensitiveError) { - throw new ApiError(meta.errors.restrictedByRole); + if (file.isSensitiveByModerator) { + throw new ApiError(meta.errors.restrictedByModerator); + } else { + throw new ApiError(meta.errors.restrictedByRole); + } } else { throw e; } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 4d17fe656..c964edbef 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -105,7 +105,7 @@ export default abstract class Channel { }); } - public abstract init(params: any): void; + public abstract init(params: any): Promise | void; public dispose?(): void; diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index beaabc610..0f071cea8 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -215,7 +215,7 @@ class OAuth2Store { } async load(req: OAuth2DecisionRequest, cb: (err: Error | null, txn?: OAuth2) => void): Promise { - const { transaction_id } = req.body; + const { transaction_id } = req.body ?? {}; if (!transaction_id) { cb(new AuthorizationError('Missing transaction ID', 'invalid_request')); return; diff --git a/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts b/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts index dbf004e27..63ca46ddb 100644 --- a/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts +++ b/packages/backend/src/server/sso/SAMLIdentifyProviderService.ts @@ -239,7 +239,7 @@ export class SAMLIdentifyProviderService { metadata: await this.createIdPMetadataXml(ssoServiceProvider), privateKey: await jose .importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}')) - .then(k => jose.exportPKCS8(k as jose.KeyLike)), + .then(k => jose.exportPKCS8(k as jose.CryptoKey)), }); const sp = saml.ServiceProvider({ @@ -393,7 +393,7 @@ export class SAMLIdentifyProviderService { metadata: await this.createIdPMetadataXml(ssoServiceProvider), privateKey: await jose .importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}')) - .then(k => jose.exportPKCS8(k as jose.KeyLike)), + .then(k => jose.exportPKCS8(k as jose.CryptoKey)), loginResponseTemplate: { context: 'ignored' }, }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index f253ab507..af2c0acc3 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,6 +26,7 @@ import type { MiNote } from '@/models/Note.js'; * noteScheduled - 予約投稿が予約された * scheduledNotePosted - 予約投稿が投稿された * scheduledNoteError - 予約投稿がエラーになった + * sensitiveFlagAssigned - センシティブフラグが付与された * app - アプリ通知 * test - テスト通知(サーバー側) */ @@ -45,6 +46,7 @@ export const notificationTypes = [ 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError', + 'sensitiveFlagAssigned', 'app', 'test', ] as const; diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 87a8378b7..29a54afe1 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -119,6 +119,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, bob); await api('following/update', { userId: bob.id, withReplies: true }, alice); await sleep(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 85c68d42b..72b37fcc6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -802,10 +802,10 @@ describe('ユーザー', () => { { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true }, + // 無効なテスト { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true }, { label: '承認制ユーザーが含まれる', user: () => userLocking }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, + // 無効なテスト { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index 6010fcee8..e1a1396b2 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -95,6 +95,7 @@ describe('NoteCreateService', () => { folderId: null, folder: null, isSensitive: false, + isSensitiveByModerator: false, maybeSensitive: false, maybePorn: false, isLink: false, diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 573cafad6..7feba0d0d 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -141,7 +141,10 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre const res = await api('notes/create', q, user); - return res.body ? res.body.createdNote : null; + assert.strictEqual(res.status, 200); + assert.notEqual(res.body, null); + + return res.body.createdNote; }; export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { diff --git a/packages/frontend/@types/vue-gtag.d.ts b/packages/frontend/@types/vue-gtag.d.ts deleted file mode 100644 index 4e4b2199f..000000000 --- a/packages/frontend/@types/vue-gtag.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'vue-gtag' { - export type GtagConsent = (command: 'consent', arg: 'default' | 'update', params: GtagConsentParams) => void; - - export interface GtagConsentParams { - ad_storage?: 'granted' | 'denied', - ad_user_data?: 'granted' | 'denied', - ad_personalization?: 'granted' | 'denied', - analytics_storage?: 'granted' | 'denied', - functionality_storage?: 'granted' | 'denied', - personalization_storage?: 'granted' | 'denied', - security_storage?: 'granted' | 'denied', - wait_for_update?: number - } -} diff --git a/packages/frontend/assets/sounds/flush.mp3 b/packages/frontend/assets/sounds/flush.mp3 new file mode 100644 index 000000000..9ec98f9fd Binary files /dev/null and b/packages/frontend/assets/sounds/flush.mp3 differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 742fe7d73..4fe102fa7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -26,23 +26,23 @@ "@rollup/plugin-typescript": "12.1.2", "@rollup/pluginutils": "5.1.4", "@syuilo/aiscript": "0.19.0", - "@tabler/icons-webfont": "3.29.0", + "@tabler/icons-webfont": "3.31.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.1", + "@vitejs/plugin-vue": "5.2.3", "@vue/compiler-sfc": "3.5.13", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "astring": "1.9.0", - "broadcast-channel": "7.0.0", + "broadcast-channel": "7.1.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", - "chart.js": "4.4.7", + "chart.js": "4.4.8", "chartjs-adapter-date-fns": "3.0.0", - "chartjs-chart-matrix": "2.0.1", + "chartjs-chart-matrix": "3.0.0", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.2.0", - "chromatic": "11.25.2", + "chromatic": "11.27.0", "compare-versions": "6.1.1", - "cropperjs": "2.0.0-rc.0", + "cropperjs": "2.0.0", "date-fns": "4.1.0", "escape-regexp": "0.0.1", "estree-walker": "3.0.3", @@ -58,88 +58,88 @@ "misskey-reversi": "workspace:*", "photoswipe": "5.4.4", "punycode.js": "2.3.1", - "rollup": "4.34.0", - "sanitize-html": "2.14.0", - "sass": "1.83.4", - "shiki": "2.2.0", + "rollup": "4.38.0", + "sanitize-html": "2.15.0", + "sass": "1.86.0", + "shiki": "3.2.1", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.173.0", + "three": "0.175.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.10", + "tsc-alias": "1.8.13", "tsconfig-paths": "4.2.0", - "typescript": "5.7.3", - "uuid": "11.0.5", + "typescript": "5.8.2", + "uuid": "11.1.0", "v-code-diff": "1.13.1", - "vite": "6.0.11", + "vite": "6.2.3", "vue": "3.5.13", - "vue-gtag": "2.0.1", + "vue-gtag": "3.1.1", "vuedraggable": "next", "webgl-audiovisualizer": "github:tar-bin/webgl-audiovisualizer" }, "devDependencies": { "@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3", - "@storybook/addon-actions": "8.5.2", - "@storybook/addon-essentials": "8.5.2", - "@storybook/addon-interactions": "8.5.2", - "@storybook/addon-links": "8.5.2", - "@storybook/addon-mdx-gfm": "8.5.2", - "@storybook/addon-storysource": "8.5.2", - "@storybook/blocks": "8.5.2", - "@storybook/components": "8.5.2", - "@storybook/core-events": "8.5.2", - "@storybook/manager-api": "8.5.2", - "@storybook/preview-api": "8.5.2", - "@storybook/react": "8.5.2", - "@storybook/react-vite": "8.5.2", - "@storybook/test": "8.5.2", - "@storybook/theming": "8.5.2", - "@storybook/types": "8.5.2", - "@storybook/vue3": "8.5.2", - "@storybook/vue3-vite": "8.5.2", + "@storybook/addon-actions": "8.6.11", + "@storybook/addon-essentials": "8.6.11", + "@storybook/addon-interactions": "8.6.11", + "@storybook/addon-links": "8.6.11", + "@storybook/addon-mdx-gfm": "8.6.11", + "@storybook/addon-storysource": "8.6.11", + "@storybook/blocks": "8.6.11", + "@storybook/components": "8.6.11", + "@storybook/core-events": "8.6.11", + "@storybook/manager-api": "8.6.11", + "@storybook/preview-api": "8.6.11", + "@storybook/react": "8.6.11", + "@storybook/react-vite": "8.6.11", + "@storybook/test": "8.6.11", + "@storybook/theming": "8.6.11", + "@storybook/types": "8.6.11", + "@storybook/vue3": "8.6.11", + "@storybook/vue3-vite": "8.6.11", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "^1.6.4", "@types/escape-regexp": "0.0.3", - "@types/estree": "1.0.6", + "@types/estree": "1.0.7", "@types/matter-js": "0.19.8", "@types/micromatch": "4.0.9", - "@types/node": "22.13.0", + "@types/node": "22.13.14", "@types/punycode.js": "npm:@types/punycode@2.1.4", - "@types/sanitize-html": "2.13.0", - "@types/three": "0.173.0", + "@types/sanitize-html": "2.15.0", + "@types/three": "0.175.0", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@types/ws": "8.5.14", + "@types/ws": "8.18.0", "@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/parser": "7.10.0", - "@vitest/coverage-v8": "3.0.4", + "@vitest/coverage-v8": "3.0.9", "@vue/runtime-core": "3.5.13", - "acorn": "8.14.0", + "acorn": "8.14.1", "cross-env": "7.0.3", - "cypress": "14.0.1", + "cypress": "14.2.1", "eslint": "8.57.1", "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "9.32.0", "fast-glob": "3.3.3", - "happy-dom": "16.8.1", + "happy-dom": "17.4.4", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.7.0", + "msw": "2.7.3", "msw-storybook-addon": "2.0.4", "nodemon": "3.1.9", - "prettier": "3.4.2", - "react": "19.0.0", - "react-dom": "19.0.0", - "start-server-and-test": "2.0.10", - "storybook": "8.5.2", + "prettier": "3.5.3", + "react": "19.1.0", + "react-dom": "19.1.0", + "start-server-and-test": "2.0.11", + "storybook": "8.6.11", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", - "vitest": "3.0.4", + "vitest": "3.0.9", "vitest-fetch-mock": "0.3.0", - "vue-component-type-helpers": "2.2.0", + "vue-component-type-helpers": "2.2.8", "vue-eslint-parser": "9.4.3", - "vue-tsc": "2.2.0" + "vue-tsc": "2.2.8" } } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 0367c79be..c63e9ba8b 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -25,7 +25,8 @@ import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { setupRouter } from '@/router/definition.js'; import { mainRouter } from '@/router/main.js'; -import VueGtag, { bootstrap as gtagBootstrap, GtagConsent, GtagConsentParams } from 'vue-gtag'; +import { createGtag, addGtag, consent as gtagConsent } from 'vue-gtag'; +import type { GtagConsentParams } from '@/types/gtag.js'; export async function common(createVue: () => App) { console.info(`Misskey v${version}`); @@ -283,21 +284,21 @@ export async function common(createVue: () => App) { components(app); if (instance.googleAnalyticsId) { - app.use(VueGtag, { - bootstrap: false, - appName: `Misskey v${version}`, - pageTrackerEnabled: true, - pageTrackerScreenviewEnabled: true, + app.use(createGtag( { + tagId: instance.googleAnalyticsId, config: { - id: instance.googleAnalyticsId, - params: { - anonymize_ip: false, - send_page_view: true, - }, + anonymize_ip: false, + send_page_view: true, }, - }, mainRouter); + pageTracker: { + router: mainRouter, + useScreenview: true, + }, + initMode: 'manual', + appName: `Misskey v${version}`, + })); - const gtagConsent = miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams ?? { + const gtagConsentParams = miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams ?? { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', @@ -306,13 +307,12 @@ export async function common(createVue: () => App) { personalization_storage: 'denied', security_storage: 'granted', }; - miLocalStorage.setItemAsJson('gtagConsent', gtagConsent); - - if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'default', gtagConsent); + miLocalStorage.setItemAsJson('gtagConsent', gtagConsentParams); + gtagConsent('default', gtagConsentParams); if (miLocalStorage.getItem('gaConsent') === 'true') { // noinspection ES6MissingAwait - gtagBootstrap(); + addGtag(); } } diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 7c9463af2..16c2a136f 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -11,6 +11,7 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; +import { isAprilFoolsDay } from '@/scripts/seasonal-events'; export default defineComponent({ props: { @@ -136,14 +137,15 @@ export default defineComponent({ [$style['date-separated-list-nogap']]: props.noGap, [$style['direction-down']]: props.direction === 'down', [$style['direction-up']]: props.direction === 'up', + [$style['april-fool']]: defaultStore.state.animation ? isAprilFoolsDay() : false, }; return () => defaultStore.state.animation ? h(TransitionGroup, { class: classes, name: 'list', tag: 'div', - onBeforeLeave, - onLeaveCancelled, + onBeforeLeave: !isAprilFoolsDay() ? onBeforeLeave : undefined, + onLeaveCancelled: !isAprilFoolsDay() ? onLeaveCancelled : undefined, }, { default: renderChildren }) : h('div', { class: classes, }, { default: renderChildren }); @@ -210,6 +212,13 @@ export default defineComponent({ } } +.april-fool { + &:global > .list-enter-from, + &:global > .list-leave-to { + animation: global-spin-shrink 3s ease-in forwards; + } +} + .separator { text-align: center; } diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 87418961b..ce14517ac 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -8,6 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ +
+
@@ -71,6 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }} {{ notification.header }} + {{ i18n.ts._notification.sensitiveFlagAssigned }}
@@ -159,6 +168,10 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + {{ i18n.ts.thisInfoIsNotVisibleOtherUser }} + @@ -341,6 +354,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_sensitiveFlagAssigned { + padding: 3px; + background: var(--eventOther); + pointer-events: none; +} + .tail { flex: 1; min-width: 0; @@ -430,6 +449,42 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) color: #fff; } +.iconFrame { + position: relative; + width: 100%; + height: 100%; + padding: 4px; + border-radius: 100%; + box-sizing: border-box; + pointer-events: none; + user-select: none; + filter: drop-shadow(0px 2px 2px #00000044); + box-shadow: 0 1px 0px #ffffff88 inset; + overflow: clip; + background: linear-gradient(0deg, #703827, #d37566); +} + +.iconImg { + width: calc(100% - 12px); + height: calc(100% - 12px); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + filter: drop-shadow(0px 1px 2px #000000aa); +} + +.iconInner { + position: relative; + width: 100%; + height: 100%; + border-radius: 100%; + box-shadow: 0 1px 0px #ffffff88 inset; + background: linear-gradient(0deg, #d37566, #703827); +} + @container (max-width: 600px) { .root { padding: 16px; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index f35c39d2f..8beca5dab 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index daf0becde..f64046d52 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only