Merge pull request #17230 from misskey-dev/develop

Release: 2026.3.1
This commit is contained in:
misskey-release-bot[bot] 2026-03-09 01:03:00 +00:00 committed by GitHub
commit 9c0e3e7937
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 2297 additions and 2062 deletions

View file

@ -16,13 +16,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -12,9 +12,9 @@ jobs:
steps:
- name: Checkout head
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Setup Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'

View file

@ -18,7 +18,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
with:
submodules: true
persist-credentials: false
@ -29,7 +29,7 @@ jobs:
- name: setup node
id: setup-node
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: pnpm
@ -66,7 +66,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps:
- name: checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
with:
submodules: true
persist-credentials: false

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Check version
run: |
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Check
run: |
counter=0

View file

@ -10,7 +10,7 @@ jobs:
check_copyright_year:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
- run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!"

View file

@ -28,7 +28,7 @@ jobs:
wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Check allowed users
id: check-allowed-users

View file

@ -27,7 +27,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub

View file

@ -32,7 +32,7 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta

View file

@ -17,7 +17,7 @@ jobs:
DOCKLE_VERSION: 0.4.15
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |

View file

@ -25,14 +25,14 @@ jobs:
ref: refs/pull/${{ github.event.number }}/merge
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -40,14 +40,14 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -36,13 +36,13 @@ jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v6.1.0
- uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -69,13 +69,13 @@ jobs:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v6.1.0
- uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -100,13 +100,13 @@ jobs:
- sw
- misskey-js
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v6.1.0
- uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v6.1.0
- uses: actions/setup-node@v6.2.0
with:
node-version-file: ".node-version"
cache: "pnpm"

View file

@ -16,13 +16,13 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -22,12 +22,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
@ -39,7 +39,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -49,7 +49,7 @@ jobs:
ports:
- 56312:6379
meilisearch:
image: getmeili/meilisearch:v1.3.4
image: getmeili/meilisearch:v1.36.0
ports:
- 57712:7700
env:
@ -57,7 +57,7 @@ jobs:
MEILI_ENV: development
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
@ -93,7 +93,7 @@ jobs:
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@ -136,13 +136,13 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@ -180,7 +180,7 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
@ -189,7 +189,7 @@ jobs:
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'

View file

@ -68,7 +68,7 @@ jobs:
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'

View file

@ -28,13 +28,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -76,7 +76,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150
@ -88,7 +88,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -22,13 +22,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.1
- uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v6.1.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View file

@ -1,3 +1,12 @@
## 2026.3.1
### General
- 依存関係の更新
### Server
- Fix: セキュリティに関する修正
## 2026.3.0
### Note

View file

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2026.3.0",
"version": "2026.3.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.30.3",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
@ -59,24 +59,24 @@
"ignore-walk": "8.0.0",
"js-yaml": "4.1.1",
"postcss": "8.5.6",
"tar": "7.5.9",
"tar": "7.5.10",
"terser": "5.46.0"
},
"devDependencies": {
"@eslint/js": "9.39.3",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@typescript/native-preview": "7.0.0-dev.20260116.1",
"cross-env": "10.1.0",
"cypress": "15.10.0",
"cypress": "15.11.0",
"eslint": "9.39.3",
"globals": "17.3.0",
"globals": "17.4.0",
"ncp": "2.0.0",
"pnpm": "10.30.1",
"start-server-and-test": "2.1.3",
"pnpm": "10.30.3",
"start-server-and-test": "2.1.5",
"typescript": "5.9.3"
},
"optionalDependencies": {

View file

@ -41,17 +41,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.15.11",
"@swc/core-darwin-x64": "1.15.11",
"@swc/core-darwin-arm64": "1.15.18",
"@swc/core-darwin-x64": "1.15.18",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.15.11",
"@swc/core-linux-arm64-gnu": "1.15.11",
"@swc/core-linux-arm64-musl": "1.15.11",
"@swc/core-linux-x64-gnu": "1.15.11",
"@swc/core-linux-x64-musl": "1.15.11",
"@swc/core-win32-arm64-msvc": "1.15.11",
"@swc/core-win32-ia32-msvc": "1.15.11",
"@swc/core-win32-x64-msvc": "1.15.11",
"@swc/core-linux-arm-gnueabihf": "1.15.18",
"@swc/core-linux-arm64-gnu": "1.15.18",
"@swc/core-linux-arm64-musl": "1.15.18",
"@swc/core-linux-x64-gnu": "1.15.18",
"@swc/core-linux-x64-musl": "1.15.18",
"@swc/core-win32-arm64-msvc": "1.15.18",
"@swc/core-win32-ia32-msvc": "1.15.18",
"@swc/core-win32-x64-msvc": "1.15.18",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.1.0",
@ -71,8 +71,8 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.995.0",
"@aws-sdk/lib-storage": "3.995.0",
"@aws-sdk/client-s3": "3.1000.0",
"@aws-sdk/lib-storage": "3.1000.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
@ -83,18 +83,18 @@
"@kitajs/html": "4.2.13",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
"@napi-rs/canvas": "0.1.94",
"@napi-rs/canvas": "0.1.95",
"@nestjs/common": "11.1.14",
"@nestjs/core": "11.1.14",
"@nestjs/testing": "11.1.14",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.39.0",
"@sentry/profiling-node": "10.39.0",
"@simplewebauthn/server": "13.2.2",
"@sentry/node": "10.40.0",
"@sentry/profiling-node": "10.40.0",
"@simplewebauthn/server": "13.2.3",
"@sinonjs/fake-timers": "15.1.0",
"@smithy/node-http-handler": "4.4.10",
"@smithy/node-http-handler": "4.4.12",
"@swc/cli": "0.8.0",
"@swc/core": "1.15.11",
"@swc/core": "1.15.18",
"@twemoji/parser": "16.0.0",
"accepts": "1.3.8",
"ajv": "8.18.0",
@ -103,7 +103,7 @@
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.2",
"bullmq": "5.69.4",
"bullmq": "5.70.1",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@ -112,7 +112,7 @@
"content-disposition": "1.0.1",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
"fastify": "5.7.4",
"fastify": "5.8.1",
"fastify-raw-body": "5.0.0",
"feed": "5.2.0",
"file-type": "21.3.0",
@ -122,7 +122,7 @@
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
"ioredis": "5.9.3",
"ioredis": "5.10.0",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0",
"is-svg": "6.1.0",
@ -145,7 +145,7 @@
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.5.0",
"pg": "8.18.0",
"pg": "8.19.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -179,7 +179,7 @@
"@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.14",
"@sentry/vue": "10.39.0",
"@sentry/vue": "10.40.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@ -193,11 +193,11 @@
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/nodemailer": "7.0.11",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.16.0",
"@types/pg": "8.18.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
@ -212,8 +212,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.11",
"cross-env": "10.1.0",

View file

@ -129,6 +129,9 @@ export interface NoteEventTypes {
type NoteStreamEventTypes = {
[key in keyof NoteEventTypes]: {
id: MiNote['id'];
userId: MiNote['userId'];
visibility: MiNote['visibility'];
visibleUserIds: MiNote['visibleUserIds'];
body: NoteEventTypes[key];
};
};
@ -378,9 +381,12 @@ export class GlobalEventService {
}
@bindThis
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
public publishNoteStream<K extends keyof NoteEventTypes>(note: MiNote, type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${note.id}`, type, {
id: note.id,
userId: note.userId,
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
body: value,
});
}

View file

@ -68,7 +68,7 @@ export class NoteDeleteService {
}
if (!quiet) {
this.globalEventService.publishNoteStream(note.id, 'deleted', {
this.globalEventService.publishNoteStream(note, 'deleted', {
deletedAt: deletedAt,
});

View file

@ -83,7 +83,7 @@ export class PollService {
const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note, 'pollVoted', {
choice: choice,
userId: user.id,
});

View file

@ -259,7 +259,7 @@ export class QueryService {
@bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
// This code must always be synchronized with the checks in NoteEntityService.isVisibleForMe and Stream abstract class Channel.isNoteVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => {
qb

View file

@ -244,7 +244,7 @@ export class ReactionService {
},
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
this.globalEventService.publishNoteStream(note, 'reacted', {
reaction: decodedReaction.reaction,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
@ -318,7 +318,7 @@ export class ReactionService {
.execute();
}
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
this.globalEventService.publishNoteStream(note, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id,
});

View file

@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { CacheService } from '@/core/CacheService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
@ -66,6 +67,7 @@ export class NoteEntityService implements OnModuleInit {
private reactionService: ReactionService;
private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService;
private cacheService: CacheService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor(
@ -101,6 +103,7 @@ export class NoteEntityService implements OnModuleInit {
//private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
//private cacheService: CacheService,
) {
}
@ -111,6 +114,7 @@ export class NoteEntityService implements OnModuleInit {
this.reactionService = this.moduleRef.get('ReactionService');
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService');
this.cacheService = this.moduleRef.get('CacheService');
}
@bindThis
@ -125,75 +129,65 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
public async shouldHideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<boolean> {
if (meId === packedNote.userId) return false;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
return true;
}
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
return true;
}
// visibility が specified かつ自分が指定されていなかったら非表示
if (!hide) {
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (packedNote.visibility === 'specified') {
if (meId == null) {
return true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (!specified) {
hide = true;
}
if (!specified) {
return true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (!hide) {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,
},
});
hide = !isFollowing;
if (packedNote.visibility === 'followers') {
if (meId == null) {
return true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
return false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
return false;
} else {
// フォロワーかどうか
const followings = await this.cacheService.userFollowingsCache.fetch(meId);
if (!Object.hasOwn(followings, packedNote.userId)) {
return true;
}
}
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
return false;
}
@bindThis
public hideNote(packedNote: Packed<'Note'>): void {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
@bindThis
@ -278,7 +272,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
@ -468,8 +462,8 @@ export class NoteEntityService implements OnModuleInit {
this.treatVisibility(packed);
if (!opts.skipHide) {
await this.hideNote(packed, meId);
if (!opts.skipHide && await this.shouldHideNote(packed, meId)) {
this.hideNote(packed);
}
return packed;

View file

@ -30,9 +30,9 @@ import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@ -131,6 +131,7 @@ export class ActivityPubServerService {
if (signature.params.headers.indexOf('digest') === -1) {
// Digest not found.
reply.code(401);
return;
} else {
const digest = request.headers.digest;

View file

@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
import { ReversiChannel } from './api/stream/channels/reversi.js';
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
@ -98,6 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
QueueStatsChannel,
ServerStatsChannel,
UserListChannel,
NoteStreamingHidingService,
OpenApiServerService,
OAuth2ProviderService,
],

View file

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const userExist = await this.usersRepository.exists({ where: { id: me.id } });
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file === null) throw new ApiError(meta.errors.noSuchFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));

View file

@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View file

@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View file

@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View file

@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, userId: me.id });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);

View file

@ -155,7 +155,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const index = ps.choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note, 'pollVoted', {
choice: ps.choice,
userId: me.id,
});

View file

@ -1,60 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { HybridTimelineChannel } from './channels/hybrid-timeline.js';
import { LocalTimelineChannel } from './channels/local-timeline.js';
import { HomeTimelineChannel } from './channels/home-timeline.js';
import { GlobalTimelineChannel } from './channels/global-timeline.js';
import { MainChannel } from './channels/main.js';
import { ChannelChannel } from './channels/channel.js';
import { AdminChannel } from './channels/admin.js';
import { ServerStatsChannel } from './channels/server-stats.js';
import { QueueStatsChannel } from './channels/queue-stats.js';
import { UserListChannel } from './channels/user-list.js';
import { AntennaChannel } from './channels/antenna.js';
import { DriveChannel } from './channels/drive.js';
import { HashtagChannel } from './channels/hashtag.js';
import { RoleTimelineChannel } from './channels/role-timeline.js';
import { ChatUserChannel } from './channels/chat-user.js';
import { ChatRoomChannel } from './channels/chat-room.js';
import { ReversiChannel } from './channels/reversi.js';
import { ReversiGameChannel } from './channels/reversi-game.js';
import type { ChannelConstructor } from './channel.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ChannelsService {
constructor(
) {
}
@bindThis
public getChannelConstructor(name: string): ChannelConstructor<boolean> {
switch (name) {
case 'main': return MainChannel;
case 'homeTimeline': return HomeTimelineChannel;
case 'localTimeline': return LocalTimelineChannel;
case 'hybridTimeline': return HybridTimelineChannel;
case 'globalTimeline': return GlobalTimelineChannel;
case 'userList': return UserListChannel;
case 'hashtag': return HashtagChannel;
case 'roleTimeline': return RoleTimelineChannel;
case 'antenna': return AntennaChannel;
case 'channel': return ChannelChannel;
case 'drive': return DriveChannel;
case 'serverStats': return ServerStatsChannel;
case 'queueStats': return QueueStatsChannel;
case 'admin': return AdminChannel;
case 'chatUser': return ChatUserChannel;
case 'chatRoom': return ChatRoomChannel;
case 'reversi': return ReversiChannel;
case 'reversiGame': return ReversiGameChannel;
default:
throw new Error(`no such channel: ${name}`);
}
}
}

View file

@ -52,7 +52,7 @@ export default class Connection {
public token?: MiAccessToken;
private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private channels: Map<string, Channel> = new Map();
private subscribingNotes: Partial<Record<string, number>> = {};
public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@ -206,6 +206,14 @@ export default class Connection {
@bindThis
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
if (data.body.visibility === 'specified' && !data.body.visibleUserIds.includes(this.user!.id)) {
return;
}
if (data.body.visibility === 'followers' && !Object.hasOwn(this.following, data.body.userId)) {
return;
}
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,
@ -254,7 +262,11 @@ export default class Connection {
*/
@bindThis
public async connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
if (this.channels.has(id)) {
this.disconnectChannel(id);
}
if (this.channels.size >= MAX_CHANNELS_PER_CONNECTION) {
return;
}
@ -270,8 +282,12 @@ export default class Connection {
}
// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
if (channelConstructor.shouldShare && this.channels.some(c => c.chName === channel)) {
return;
if (channelConstructor.shouldShare) {
for (const c of this.channels.values()) {
if (c.chName === channel) {
return;
}
}
}
const contextId = ContextIdFactory.create();
@ -281,8 +297,13 @@ export default class Connection {
}, contextId);
const ch: Channel = await this.moduleRef.create<Channel>(channelConstructor, contextId);
this.channels.push(ch);
ch.init(params ?? {});
this.channels.set(ch.id, ch);
const valid = await ch.init(params ?? {});
if (typeof valid === 'boolean' && !valid) {
// 初期化処理の結果、接続拒否されたので切断
this.disconnectChannel(id);
return;
}
if (pong) {
this.sendMessageToWs('connected', {
@ -324,11 +345,11 @@ export default class Connection {
*/
@bindThis
public disconnectChannel(id: string) {
const channel = this.channels.find(c => c.id === id);
const channel = this.channels.get(id);
if (channel) {
if (channel.dispose) channel.dispose();
this.channels = this.channels.filter(c => c.id !== id);
this.channels.delete(id);
}
}
@ -343,7 +364,7 @@ export default class Connection {
if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return;
const channel = this.channels.find(c => c.id === data.id);
const channel = this.channels.get(data.id);
if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body);
}
@ -355,7 +376,7 @@ export default class Connection {
@bindThis
public dispose() {
if (this.fetchIntervalId) clearInterval(this.fetchIntervalId);
for (const c of this.channels.filter(c => c.dispose)) {
for (const c of this.channels.values()) {
if (c.dispose) c.dispose();
}
}

View file

@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiUser } from '@/models/User.js';
type HiddenLayer = 'note' | 'renote' | 'renoteRenote';
type LockdownCheckResult =
| { shouldSkip: true }
| { shouldSkip: false; hiddenLayers: Set<HiddenLayer> };
/** Streamにおいて、ートを隠すhideNoteを適用するためのService */
@Injectable()
export class NoteStreamingHidingService {
constructor(
private noteEntityService: NoteEntityService,
) {}
/**
*
*
* @param note -
* @param meId - IDnull
* @returns shouldSkip: true false hiddenLayers
*/
@bindThis
public async shouldHide(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<LockdownCheckResult> {
const hiddenLayers = new Set<HiddenLayer>();
// 1階層目: note自体
const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId);
if (shouldHideThisNote) {
if (isRenotePacked(note) && isQuotePacked(note)) {
// 引用リノートの場合、内容を隠して流す
hiddenLayers.add('note');
} else if (isRenotePacked(note)) {
// 純粋リノートの場合、流さない
return { shouldSkip: true };
} else {
// 通常ノートの場合、内容を隠して流す
hiddenLayers.add('note');
}
}
// 2階層目: note.renote
if (isRenotePacked(note) && note.renote) {
const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId);
if (shouldHideRenote) {
if (isQuotePacked(note)) {
// noteが引用リートの場合、renote部分だけ隠す
hiddenLayers.add('renote');
} else {
// noteが純粋リートの場合、流さない
return { shouldSkip: true };
}
}
}
// 3階層目: note.renote.renote
if (isRenotePacked(note) && note.renote &&
isRenotePacked(note.renote) && note.renote.renote) {
const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId);
if (shouldHideRenoteRenote) {
if (isQuotePacked(note.renote)) {
// note.renoteが引用リートの場合、renote.renote部分だけ隠す
hiddenLayers.add('renoteRenote');
} else {
// note.renoteが純粋リートの場合、note.renoteの意味がなくなるので流さない
return { shouldSkip: true };
}
}
}
return { shouldSkip: false, hiddenLayers };
}
/**
* hiddenLayersに基づいてートの内容を隠す
*
* `note`
*
* @param note -
* @param hiddenLayers -
*/
@bindThis
public applyHiding(
note: Packed<'Note'>,
hiddenLayers: Set<HiddenLayer>,
): void {
if (hiddenLayers.has('note')) {
this.noteEntityService.hideNote(note);
}
if (hiddenLayers.has('renote') && note.renote) {
this.noteEntityService.hideNote(note.renote);
}
if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) {
this.noteEntityService.hideNote(note.renote.renote);
}
}
/**
*
*
* `note`
*
* @param note -
* @param meId - IDnull
* @returns shouldSkip: true
*/
@bindThis
public async processHiding(
note: Packed<'Note'>,
meId: MiUser['id'] | null,
): Promise<{ shouldSkip: boolean }> {
const result = await this.shouldHide(note, meId);
if (result.shouldSkip) {
return { shouldSkip: true };
}
this.applyHiding(note, result.hiddenLayers);
return { shouldSkip: false };
}
}

View file

@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import { isChannelRelated } from '@/misc/is-channel-related.js';
import type { Awaitable } from '@/types.js';
import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js';
@ -64,6 +65,43 @@ export default abstract class Channel {
return this.connection.subscriber;
}
protected isNoteVisibleForMe(note: Packed<'Note'>): boolean {
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
const meId = this.connection.user?.id ?? null;
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
return note.visibleUserIds?.some(id => meId === id) ?? false;
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
return Object.hasOwn(this.following, note.userId);
}
}
return true;
}
/*
*
*/
@ -104,7 +142,14 @@ export default abstract class Channel {
});
}
public abstract init(params: JsonObject): void;
/**
*
*
* - `void / Promise<void>`
* - `true / Promise<true>`
* - `false / Promise<false>`
*/
public abstract init(params: JsonObject): Awaitable<void | boolean>;
public dispose?(): void;

View file

@ -4,8 +4,12 @@
*/
import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennasRepository } from '@/models/_.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@ -23,19 +27,36 @@ export class AntennaChannel extends Channel {
@Inject(REQUEST)
request: ChannelRequest,
@Inject(DI.antennasRepository)
private antennasReposiotry: AntennasRepository,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onEvent = this.onEvent.bind(this);
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.antennaId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.antennaId !== 'string') return false;
if (!this.user) return false;
this.antennaId = params.antennaId;
const antennaExists = await this.antennasReposiotry.exists({
where: {
id: this.antennaId,
userId: this.user.id,
},
});
if (!antennaExists) return false;
// Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
return true;
}
@bindThis
@ -43,8 +64,21 @@ export class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View file

@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@ -26,6 +27,7 @@ export class ChannelChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -48,12 +50,18 @@ export class ChannelChannel extends Channel {
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -4,12 +4,14 @@
*/
import { Inject, Injectable, Scope } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { ChatService } from '@/core/ChatService.js';
import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core';
import type { ChatRoomsRepository } from '@/models/_.js';
@Injectable({ scope: Scope.TRANSIENT })
export class ChatRoomChannel extends Channel {
@ -23,17 +25,31 @@ export class ChatRoomChannel extends Channel {
@Inject(REQUEST)
request: ChannelRequest,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
private chatService: ChatService,
) {
super(request);
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.roomId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.roomId !== 'string') return false;
if (!this.user) return false;
this.roomId = params.roomId;
const room = await this.chatRoomsRepository.findOneBy({
id: this.roomId,
});
if (room == null) return false;
if (!(await this.chatService.hasPermissionToViewRoomTimeline(this.user.id, room))) return false;
this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent);
return true;
}
@bindThis

View file

@ -29,11 +29,16 @@ export class ChatUserChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.otherId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.otherId !== 'string') return false;
if (!this.user) return false;
if (params.otherId === this.user.id) return false;
this.otherId = params.otherId;
this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
this.subscriber.on(`chatUserStream:${this.user.id}-${this.otherId}`, this.onEvent);
return true;
}
@bindThis

View file

@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@ -29,6 +30,7 @@ export class GlobalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -60,10 +62,15 @@ export class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -7,12 +7,12 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class HashtagChannel extends Channel {
public readonly chName = 'hashtag';
@ -25,19 +25,26 @@ export class HashtagChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: JsonObject) {
if (!Array.isArray(params.q)) return;
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
public async init(params: JsonObject): Promise<boolean> {
if (!Array.isArray(params.q)) return false;
if (!params.q.every((x): x is string[] => (
Array.isArray(x) &&
x.length >= 1 &&
x.every(y => typeof y === 'string')
))) return false;
this.q = params.q;
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
return true;
}
@bindThis
@ -46,12 +53,21 @@ export class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
if (!this.isNoteVisibleForMe(note)) return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -6,6 +6,7 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
@ -26,6 +27,7 @@ export class HomeTimelineChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -55,11 +57,7 @@ export class HomeTimelineChannel extends Channel {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@ -84,10 +82,15 @@ export class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@ -31,6 +32,7 @@ export class HybridTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -75,12 +77,7 @@ export class HybridTimelineChannel extends Channel {
}
}
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
@ -104,10 +101,15 @@ export class HybridTimelineChannel extends Channel {
}
}
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
@ -30,6 +31,7 @@ export class LocalTimelineChannel extends Channel {
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -70,10 +72,15 @@ export class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -28,9 +28,10 @@ export class MainChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
public async init(params: JsonObject): Promise<boolean> {
if (!this.user) return false;
this.subscriber.on(`mainStream:${this.user.id}`, async data => {
switch (data.type) {
case 'notification': {
// Ignore notifications from instances the user has muted
@ -47,8 +48,8 @@ export class MainChannel extends Channel {
}
case 'mention': {
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (this.userIdsWhoMeMuting.has(data.body.userId)) return;
if (!this.isNoteVisibleForMe(data.body)) return;
if (this.isNoteMutedOrBlocked(data.body)) return;
if (data.body.isHidden) {
const note = await this.noteEntityService.pack(data.body.id, this.user, {
detail: true,
@ -61,5 +62,7 @@ export class MainChannel extends Channel {
this.send(data.type, data.body);
});
return true;
}
}

View file

@ -7,6 +7,8 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type ChannelRequest } from '../channel.js';
@ -25,6 +27,7 @@ export class RoleTimelineChannel extends Channel {
private noteEntityService: NoteEntityService,
private roleservice: RoleService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.onNote = this.onNote.bind(this);
@ -47,9 +50,24 @@ export class RoleTimelineChannel extends Channel {
return;
}
if (note.visibility !== 'public') return;
if (note.user.requireSigninToViewContents && this.user == null) return;
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
if (this.isNoteMutedOrBlocked(note)) return;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}
this.send('note', note);
} else {
this.send(data.type, data.body);

View file

@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
@ -36,6 +37,7 @@ export class UserListChannel extends Channel {
request: ChannelRequest,
private noteEntityService: NoteEntityService,
private noteStreamingHidingService: NoteStreamingHidingService,
) {
super(request);
//this.updateListUsers = this.updateListUsers.bind(this);
@ -43,8 +45,8 @@ export class UserListChannel extends Channel {
}
@bindThis
public async init(params: JsonObject) {
if (typeof params.listId !== 'string') return;
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.listId !== 'string') return false;
this.listId = params.listId;
this.withFiles = !!(params.withFiles ?? false);
this.withRenotes = !!(params.withRenotes ?? true);
@ -56,7 +58,7 @@ export class UserListChannel extends Channel {
userId: this.user!.id,
},
});
if (!listExist) return;
if (!listExist) return false;
// Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send);
@ -65,6 +67,8 @@ export class UserListChannel extends Channel {
this.updateListUsers();
this.listUsersClock = setInterval(this.updateListUsers, 5000);
return true;
}
@bindThis
@ -96,11 +100,7 @@ export class UserListChannel extends Channel {
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
if (!this.isNoteVisibleForMe(note)) return;
if (note.reply) {
const reply = note.reply;
@ -117,10 +117,15 @@ export class UserListChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
if (shouldSkip) return;
if (this.user) {
if (isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
}

View file

@ -414,3 +414,5 @@ export type FilterUnionByProperty<
Property extends string | number | symbol,
Condition,
> = Union extends Record<Property, Condition> ? Union : never;
export type Awaitable<T> = T | Promise<T>;

View file

@ -11,9 +11,9 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"rollup": "4.59.0"
},
"dependencies": {

View file

@ -27,11 +27,11 @@
"punycode.js": "2.3.1",
"rollup": "4.59.0",
"sass": "1.97.3",
"shiki": "3.22.0",
"shiki": "3.23.0",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
"vite": "7.3.1",
"vue": "3.5.28"
"vue": "3.5.29"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@ -39,14 +39,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.10",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"@vue/runtime-core": "3.5.28",
"@vue/runtime-core": "3.5.29",
"acorn": "8.16.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
@ -57,11 +57,11 @@
"msw": "2.12.10",
"nodemon": "3.1.14",
"prettier": "3.8.1",
"start-server-and-test": "2.1.3",
"start-server-and-test": "2.1.5",
"tsx": "4.21.0",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.2.4",
"vue-component-type-helpers": "3.2.5",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.4"
"vue-tsc": "3.2.5"
}
}

View file

@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"eslint-plugin-vue": "10.8.0",
"nodemon": "3.1.14",
@ -35,6 +35,6 @@
"dependencies": {
"i18n": "workspace:*",
"misskey-js": "workspace:*",
"vue": "3.5.28"
"vue": "3.5.29"
}
}

View file

@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.39.0",
"@sentry/vue": "10.40.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@ -39,7 +39,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "15.1.1",
"chromatic": "15.2.0",
"compare-versions": "6.1.1",
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
@ -55,7 +55,7 @@
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
"mediabunny": "1.34.4",
"mediabunny": "1.35.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@ -67,21 +67,21 @@
"rollup": "4.59.0",
"sanitize-html": "2.17.1",
"sass": "1.97.3",
"shiki": "3.22.0",
"shiki": "3.23.0",
"textarea-caret": "3.1.0",
"three": "0.183.1",
"three": "0.183.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
"vite": "7.3.1",
"vue": "3.5.28",
"vue": "3.5.29",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
"@storybook/addon-essentials": "8.6.17",
"@storybook/addon-interactions": "8.6.17",
"@storybook/addon-links": "10.2.10",
"@storybook/addon-links": "10.2.13",
"@storybook/addon-mdx-gfm": "8.6.17",
"@storybook/addon-storysource": "8.6.17",
"@storybook/blocks": "8.6.17",
@ -89,13 +89,13 @@
"@storybook/core-events": "8.6.17",
"@storybook/manager-api": "8.6.17",
"@storybook/preview-api": "8.6.17",
"@storybook/react": "10.2.10",
"@storybook/react-vite": "10.2.10",
"@storybook/react": "10.2.13",
"@storybook/react-vite": "10.2.13",
"@storybook/test": "8.6.17",
"@storybook/theming": "8.6.17",
"@storybook/types": "8.6.17",
"@storybook/vue3": "10.2.10",
"@storybook/vue3-vite": "10.2.10",
"@storybook/vue3": "10.2.13",
"@storybook/vue3-vite": "10.2.13",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@ -103,21 +103,21 @@
"@types/insert-text-at-cursor": "0.3.2",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"@vue/compiler-core": "3.5.28",
"@vue/compiler-core": "3.5.29",
"acorn": "8.16.0",
"astring": "1.9.0",
"cross-env": "10.1.0",
"cypress": "15.10.0",
"cypress": "15.11.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"estree-walker": "3.0.3",
@ -133,16 +133,16 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.3",
"storybook": "10.2.10",
"start-server-and-test": "2.1.5",
"storybook": "10.2.13",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0",
"vite-plugin-glsl": "1.5.5",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.0.18",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.2.4",
"vue-component-type-helpers": "3.2.5",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.4"
"vue-tsc": "3.2.5"
}
}

View file

@ -82,8 +82,10 @@ export class Pizzax<T extends StateDef> {
this.r = {} as ReactiveState<T>;
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
this.s[k] = v.default;
this.r[k] = ref(v.default);
// 参照渡しになるのを防ぐためclone
const defaultValue = deepClone(v.default);
this.s[k] = defaultValue;
this.r[k] = ref(defaultValue);
}
this.ready = this.init();
@ -120,7 +122,8 @@ export class Pizzax<T extends StateDef> {
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
this.r[k].value = this.s[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else {
this.r[k].value = this.s[k] = v.default;
// 参照渡しになるのを防ぐためclone
this.r[k].value = this.s[k] = deepClone(v.default);
}
}
@ -148,7 +151,8 @@ export class Pizzax<T extends StateDef> {
this.r[k].value = this.s[k] = (kvs as Partial<T>)[k];
cache[k] = (kvs as Partial<T>)[k];
} else {
this.r[k].value = this.s[k] = v.default;
// 参照渡しになるのを防ぐためclone
this.r[k].value = this.s[k] = deepClone(v.default);
}
}
}
@ -218,8 +222,10 @@ export class Pizzax<T extends StateDef> {
}
public reset(key: keyof T) {
this.set(key, this.def[key].default);
return this.def[key].default;
// 参照渡しになるのを防ぐためclone
const defaultValue = deepClone(this.def[key].default);
this.set(key, defaultValue);
return defaultValue;
}
/**

View file

@ -14,6 +14,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { deepEqual } from '@/utility/deep-equal.js';
import { deepClone } from '@/utility/clone.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@ -122,7 +123,8 @@ export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
if (typeof _default === 'function') { // factory
return _default() as ValueOf<K>;
} else {
return _default as unknown as ValueOf<K>;
// 参照渡しになるのを防ぐためclone
return deepClone(_default as unknown as ValueOf<K>);
}
}

View file

@ -29,9 +29,9 @@
],
"devDependencies": {
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"chokidar": "5.0.0",
"esbuild": "0.27.3",
"execa": "9.6.1",

View file

@ -11,14 +11,14 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0"
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",
"harfbuzzjs": "0.8.0",
"harfbuzzjs": "0.10.0",
"tsx": "4.21.0",
"wawoff2": "2.0.1"
},

View file

@ -25,10 +25,10 @@
},
"devDependencies": {
"@types/matter-js": "0.20.2",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"execa": "9.6.1",
"nodemon": "3.1.14"

View file

@ -8,9 +8,9 @@
},
"devDependencies": {
"@readme/openapi-parser": "5.5.0",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"openapi-types": "12.1.3",
"openapi-typescript": "7.13.0",
"ts-case-convert": "2.1.0",

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.3.0",
"version": "2026.3.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -37,10 +37,10 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.57.2",
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@microsoft/api-extractor": "7.57.6",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/coverage-v8": "4.0.18",
"esbuild": "0.27.3",
"execa": "9.6.1",

View file

@ -24,9 +24,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.10.13",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@types/node": "24.11.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"esbuild": "0.27.3",
"execa": "9.6.1",
"nodemon": "3.1.14"

View file

@ -15,7 +15,7 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/parser": "8.56.1",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.14"

3370
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,5 @@ ignorePatchFailures: false
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
minimumReleaseAgeExclude:
- '@syuilo/aiscript'
- 'minimatch' # 脆弱性対応 そのうち消す
- 'rollup' # 脆弱性対応 そのうち消す
- '@rollup/rollup-*' # 脆弱性対応 そのうち消す
- 'tar' # 脆弱性対応 そのうち消す
- 'fastify' # 脆弱性対応 そのうち消す

View file

@ -9,7 +9,7 @@
"version": "1.0.0",
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "24.10.12",
"@types/node": "24.11.0",
"@vitest/coverage-v8": "4.0.18",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
@ -917,9 +917,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "24.10.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz",
"integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==",
"version": "24.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
"integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
"dev": true,
"license": "MIT",
"dependencies": {

View file

@ -10,7 +10,7 @@
},
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "24.10.13",
"@types/node": "24.11.0",
"@vitest/coverage-v8": "4.0.18",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",