Compare commits

...

17 Commits

Author SHA1 Message Date
おさむのひと a67795a195
Merge 5b6ace0b8a into d522d1bf26 2025-05-04 12:57:52 +00:00
Sayamame-beans d522d1bf26
docs(changelog): add information for #15924 (#15947) 2025-05-04 20:59:24 +09:00
github-actions[bot] 080276e3e7 Bump version to 2025.5.0-alpha.1 2025-05-04 10:07:59 +00:00
syuilo 619bb2214e
New Crowdin updates (#15935)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)
2025-05-04 19:00:56 +09:00
renovate[bot] c23f2ff900
chore(deps): update node.js to v22.15.0 (#15606)
* chore(deps): update node.js to v22.15.0

* chore: determine Jest args from Node.js version

* fix

* fix: `import.meta.dirname` is not supported in v20.10.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-05-04 19:00:36 +09:00
syuilo 14d6734cb1
Fix MkPullToRefresh behaviour (#15944)
* Update MkPullToRefresh.vue

* Update MkPullToRefresh.vue

* Update MkPullToRefresh.vue
2025-05-04 18:51:30 +09:00
syuilo 3bdb1dd558 🎨 2025-05-04 17:32:09 +09:00
かっこかり e75d749784
fix(frontend): ダイアログのお知らせが画面からはみ出ることがある問題を修正 (#15878)
* fix(frontend): ダイアログのお知らせが画面からはみ出ることがある問題を修正

* Update Changelog

* 🎨

* 🎨

* enhance: スクロールしないと閉じられないように

* Update CHANGELOG.md
2025-05-04 15:50:05 +09:00
renovate[bot] 42a2ed8b67
fix(deps): update dependency vite to v6.3.4 [security] (#15918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 14:54:35 +09:00
zyoshoka a5fa9a2cef
chore(workflow): use `node-version-file` instead of hardcoded `node-version` (#15658)
* chore(workflow): use `node-version-file` instead of hardcoded `node-version`

* chore: test min version
2025-05-04 14:14:09 +09:00
おさむのひと 5b6ace0b8a fix import 2025-04-27 21:54:27 +09:00
おさむのひと d681dc42e8 followup merge 2025-04-27 21:53:14 +09:00
おさむのひと 4c40d0a809 Merge branch 'develop' into enhance/15556-antenna-regex
# Conflicts:
#	packages/backend/src/core/AntennaService.ts
#	packages/backend/src/models/Antenna.ts
#	packages/backend/src/server/api/endpoints/antennas/create.ts
#	packages/backend/src/server/api/endpoints/antennas/update.ts
#	packages/frontend/src/components/MkAntennaEditor.vue
2025-04-27 21:51:13 +09:00
おさむのひと fae4147904 wip 2025-03-12 19:23:40 +09:00
おさむのひと dfa66e6e2a fix 2025-03-01 15:49:17 +09:00
おさむのひと 08b8bf94ef fix 2025-03-01 08:40:35 +09:00
おさむのひと 7a8ab424d4 impl service 2025-02-28 19:56:14 +09:00
44 changed files with 779 additions and 431 deletions

View File

@ -5,7 +5,7 @@
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0"
"version": "22.15.0"
},
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "10.10.0"

1
.github/min.node-version vendored Normal file
View File

@ -0,0 +1 @@
20.10.0

View File

@ -17,7 +17,6 @@ jobs:
strategy:
matrix:
node-version: [22.11.0]
api-json-name: [api-base.json, api-head.json]
include:
- api-json-name: api-base.json
@ -32,10 +31,10 @@ jobs:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -15,22 +15,17 @@ jobs:
contents: read
id-token: write
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Publish package
run: |
pnpm i --frozen-lockfile

View File

@ -38,7 +38,7 @@ jobs:
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js 20.x
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'

View File

@ -22,10 +22,11 @@ jobs:
unit:
name: Unit tests (backend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
services:
postgres:
@ -61,10 +62,10 @@ jobs:
exit 1
fi
done
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
@ -84,10 +85,11 @@ jobs:
e2e:
name: E2E tests (backend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
services:
postgres:
@ -108,10 +110,10 @@ jobs:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -21,7 +21,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
steps:
- uses: actions/checkout@v4
with:
@ -43,10 +45,10 @@ jobs:
exit 1
fi
done
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- name: Build Misskey
run: |
@ -54,6 +56,7 @@ jobs:
pnpm build
- name: Setup
run: |
echo "NODE_VERSION=$(cat ${{ matrix.node-version-file }})" >> $GITHUB_ENV
cd packages/backend/test-federation
bash ./setup.sh
sudo chmod 644 ./certificates/*.test.key

View File

@ -27,20 +27,16 @@ jobs:
name: Unit tests (frontend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
@ -64,7 +60,6 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [22.11.0]
browser: [chrome]
services:
@ -92,10 +87,10 @@ jobs:
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Copy Configure

View File

@ -20,11 +20,6 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
@ -32,10 +27,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install dependencies

View File

@ -15,20 +15,16 @@ jobs:
name: Production build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -16,20 +16,16 @@ jobs:
validate-api-json:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install Redocly CLI
run: npm i -g @redocly/cli

View File

@ -1 +1 @@
22.11.0
22.15.0

View File

@ -1,5 +1,8 @@
## 2025.5.0
### Note
- DockerのNode.jsが22.15.0に更新されました
### General
-
@ -8,6 +11,7 @@
- アクセシビリティ設定からオフにすることもできます
- Enhance: タイムラインのパフォーマンスを向上
- Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正
- Fix: ダイアログのお知らせが画面からはみ出ることがある問題を修正
### Server
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
@ -20,6 +24,8 @@
- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175)
- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正
- Fix: ファイルのアップロードに失敗することがある問題を修正
- 投稿フォーム上で画像のクロップを行うと、`Invalid Param.`エラーでノートが投稿出来なくなる問題も解決されます。
- この事象によって既にノートが投稿出来ない状態になっている場合は、投稿フォーム右上のメニューから、下書きデータの「リセット」を行ってください。
## 2025.4.1

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=22.11.0-bookworm
ARG NODE_VERSION=22.15.0-bookworm
# build assets & compile TypeScript

View File

@ -1425,6 +1425,7 @@ _settings:
ifOff: "Quan es desactiva"
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
enablePullToRefresh: "Lliscar i actualitzar "
enablePullToRefresh_description: "Amb el ratolí, llisca mentre prems la roda."
_chat:
showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar"

View File

@ -1348,6 +1348,7 @@ readonly: "Read only"
goToDeck: "Return to Deck"
federationJobs: "Federation Jobs"
driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed. <br> \nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later. <br> \n<b>Be careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).</b> <br> \nYou can also create folders to organize your files."
scrollToClose: "Scroll to close"
_chat:
noMessagesYet: "No messages yet"
newMessage: "New message"
@ -1425,6 +1426,7 @@ _settings:
ifOff: "When turned off"
enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
enablePullToRefresh: "Pull to Refresh"
enablePullToRefresh_description: "When using a mouse, drag while pressing in the scrolling wheel."
_chat:
showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send"

12
locales/index.d.ts vendored
View File

@ -1706,6 +1706,14 @@ export interface Locale extends ILocale {
* Botアカウントを除外
*/
"antennaExcludeBots": string;
/**
* 使
*/
"antennaUseRegex": string;
/**
* {src}{line}
*/
"antennaUseRegexError": ParameterizedString<"src" | "line">;
/**
* AND指定になりOR指定になります
*/
@ -5413,6 +5421,10 @@ export interface Locale extends ILocale {
*
*/
"driveAboutTip": string;
/**
*
*/
"scrollToClose": string;
"_chat": {
/**
*

View File

@ -422,6 +422,8 @@ antennaSource: "受信ソース"
antennaKeywords: "受信キーワード"
antennaExcludeKeywords: "除外キーワード"
antennaExcludeBots: "Botアカウントを除外"
antennaUseRegex: "正規表現を使用する"
antennaUseRegexError: "{src}の{line}行目にエラーがあります。"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
@ -1348,6 +1350,7 @@ readonly: "読み取り専用"
goToDeck: "デッキへ戻る"
federationJobs: "連合ジョブ"
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
scrollToClose: "スクロールして閉じる"
_chat:
noMessagesYet: "まだメッセージはありません"

View File

@ -1348,6 +1348,7 @@ readonly: "唯讀"
goToDeck: "回去甲板"
federationJobs: "聯邦通訊作業"
driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。"
scrollToClose: "用滾輪關閉"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@ -1425,6 +1426,7 @@ _settings:
ifOff: "關閉時"
enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
enablePullToRefresh: "下拉更新"
enablePullToRefresh_description: "使用滑鼠,按下並拖曳滾輪。"
_chat:
showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.5.0-alpha.0",
"version": "2025.5.0-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -1,4 +1,5 @@
import tsParser from '@typescript-eslint/parser';
import globals from 'globals';
import sharedConfig from '../shared/eslint.config.js';
export default [
@ -6,6 +7,13 @@ export default [
{
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
},
{
languageOptions: {
globals: {
...globals.node,
},
},
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {

20
packages/backend/jest.js Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
import child_process from 'node:child_process';
import path from 'node:path';
import url from 'node:url';
import semver from 'semver';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = [];
args.push(...[
...semver.satisfies(process.version, '^20.17.0 || ^22.0.0') ? ['--no-experimental-require-module'] : [],
'--experimental-vm-modules',
'--experimental-import-meta-resolve',
path.join(__dirname, 'node_modules/jest/bin/jest.js'),
...process.argv.slice(2),
]);
child_process.spawn(process.execPath, args, { stdio: 'inherit' });

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AntennaRegexSupport1740781670204 {
name = 'AntennaRegexSupport1740781670204'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" ADD "useRegex" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "useRegex"`);
}
}

View File

@ -22,12 +22,12 @@
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
"jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache",
"test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test:fed": "pnpm jest:fed",

View File

@ -5,26 +5,161 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import RE2 from 're2';
import { In } from 'typeorm';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
import type { MiAntenna } from '@/models/Antenna.js';
import type { AntennasRepository } from '@/models/_.js';
import { AntennaSource, MiAntenna } from '@/models/Antenna.js';
import type { MiNote } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js';
import { CacheService } from './CacheService.js';
import { RoleService } from './RoleService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
type AntennaFilter = {
antennaId: string;
src: MiAntenna;
testKeywords(target: string): boolean;
};
function matchAntennaKeywords(target: string, keywords: string[][]): boolean {
return keywords.some(and => and.every(keyword => target.includes(keyword)));
}
function matchAntennaKeywordsCaseInsensitive(target: string, keywords: string[][]): boolean {
const _target = target.toLowerCase();
return keywords.some(and => and.every(keyword => _target.includes(keyword)));
}
function matchAntennaKeywordsRegex(target: string, patterns: RE2[]): boolean {
return patterns.every(re => re.test(target));
}
export const antennaFilters = {
regex: {
includeAndExclude: (target: string, keywords: RE2[], excludeKeywords: RE2[]): boolean => {
return matchAntennaKeywordsRegex(target, keywords) && !matchAntennaKeywordsRegex(target, excludeKeywords);
},
includeOnly: (target: string, keywords: RE2[]): boolean => {
return matchAntennaKeywordsRegex(target, keywords);
},
excludeOnly: (target: string, excludeKeywords: RE2[]): boolean => {
return !matchAntennaKeywordsRegex(target, excludeKeywords);
},
},
noRegex: {
caseSensitive: {
includeAndExclude: (target: string, keywords: string[][], excludeKeywords: string[][]): boolean => {
return matchAntennaKeywords(target, keywords) && !matchAntennaKeywords(target, excludeKeywords);
},
includeOnly: (target: string, keywords: string[][]): boolean => {
return matchAntennaKeywords(target, keywords);
},
excludeOnly: (target: string, excludeKeywords: string[][]): boolean => {
return !matchAntennaKeywords(target, excludeKeywords);
},
},
caseInsensitive: {
includeAndExclude: (target: string, keywords: string[][], excludeKeywords: string[][]): boolean => {
return matchAntennaKeywordsCaseInsensitive(target, keywords) && !matchAntennaKeywordsCaseInsensitive(target, excludeKeywords);
},
includeOnly: (target: string, keywords: string[][]): boolean => {
return matchAntennaKeywordsCaseInsensitive(target, keywords);
},
excludeOnly: (target: string, excludeKeywords: string[][]): boolean => {
return !matchAntennaKeywordsCaseInsensitive(target, excludeKeywords);
},
},
},
};
function alwaysTrue(): boolean {
return true;
}
export function createAntennaFilter(antenna: MiAntenna): AntennaFilter {
function createTestKeywordsFunction(antenna: MiAntenna): AntennaFilter['testKeywords'] {
// Clean up
const keywords = antenna.keywords
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
const excludeKeywords = antenna.excludeKeywords
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
if (antenna.useRegex) {
// 元々はAND検索を行うために2次元配列としてもっていた歴史的経緯がある.
// 正規表現の時は1行に付き1パターンとするため、[n][0]にパターンの内容すべてが格納されているものとして扱う.
const createRE2 = (pattern: string): RE2 => {
const regexp = pattern.match(/^\/(.+)\/(.*)$/) ?? [];
return new RE2(regexp[1], regexp[2]);
};
const keywordsPatterns = keywords.map(line => createRE2(line[0]));
const excludeKeywordsPatterns = excludeKeywords.map(line => createRE2(line[0]));
if (keywords.length > 0 && excludeKeywords.length > 0) {
return (target: string) => antennaFilters.regex.includeAndExclude(target, keywordsPatterns, excludeKeywordsPatterns);
} else if (keywords.length > 0) {
return (target: string) => antennaFilters.regex.includeOnly(target, keywordsPatterns);
} else if (excludeKeywords.length > 0) {
return (target: string) => antennaFilters.regex.excludeOnly(target, excludeKeywordsPatterns);
} else {
return alwaysTrue;
}
} else if (antenna.caseSensitive) {
if (keywords.length > 0 && excludeKeywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseSensitive.includeAndExclude(target, keywords, excludeKeywords);
} else if (keywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseSensitive.includeOnly(target, keywords);
} else if (excludeKeywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseSensitive.excludeOnly(target, excludeKeywords);
} else {
return alwaysTrue;
}
} else {
if (keywords.length > 0 && excludeKeywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseInsensitive.includeAndExclude(target, keywords, excludeKeywords);
} else if (keywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseInsensitive.includeOnly(target, keywords);
} else if (excludeKeywords.length > 0) {
return (target: string) => antennaFilters.noRegex.caseInsensitive.excludeOnly(target, excludeKeywords);
} else {
return alwaysTrue;
}
}
}
return {
antennaId: antenna.id,
src: antenna,
testKeywords: createTestKeywordsFunction(antenna),
};
}
@Injectable()
export class AntennaService implements OnApplicationShutdown {
private antennasFetched: boolean;
private antennas: MiAntenna[];
public static AntennaNotFoundError = class extends Error {
};
public static EmptyKeyWordError = class extends Error {
};
public static TooManyAntennasError = class extends Error {
};
public static InvalidRegexPatternError = class extends Error {
constructor(err?: unknown) {
const msg = err instanceof Error ? err.message : undefined;
super(msg);
}
};
private filtersFetched: boolean;
private filters: AntennaFilter[];
constructor(
@Inject(DI.redisForTimelines)
@ -36,16 +171,14 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
private cacheService: CacheService,
private roleService: RoleService,
private utilityService: UtilityService,
private globalEventService: GlobalEventService,
private fanoutTimelineService: FanoutTimelineService,
private idService: IdService,
) {
this.antennasFetched = false;
this.antennas = [];
this.filtersFetched = false;
this.filters = [];
this.redisForSub.on('message', this.onRedisMessage);
}
@ -57,37 +190,24 @@ export class AntennaService implements OnApplicationShutdown {
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'antennaCreated':
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
});
case 'antennaCreated': {
this.filters.push(createAntennaFilter(MiAntenna.deserialize(body)));
break;
}
case 'antennaUpdated': {
const idx = this.antennas.findIndex(a => a.id === body.id);
const idx = this.filters.findIndex(a => a.antennaId === body.id);
if (idx >= 0) {
this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
};
this.filters[idx] = createAntennaFilter(MiAntenna.deserialize(body));
} else {
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
lastUsedAt: new Date(body.lastUsedAt),
user: null, // joinなカラムは通常取ってこないので
userList: null, // joinなカラムは通常取ってこないので
});
this.filters.push(createAntennaFilter(MiAntenna.deserialize(body)));
}
break;
}
case 'antennaDeleted': {
this.filters = this.filters.filter(a => a.antennaId !== body.id);
break;
case 'antennaDeleted':
this.antennas = this.antennas.filter(a => a.id !== body.id);
break;
}
default:
break;
}
@ -96,128 +216,224 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
const antennas = await this.getAntennas();
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
const filters = await this.getFilters();
const redisPipeline = this.redisForTimelines.pipeline();
for (const antenna of matchedAntennas) {
this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
for (const filter of filters) {
if (this.checkHitAntenna(filter, note, noteUser)) {
this.fanoutTimelineService.push(`antennaTimeline:${filter.antennaId}`, note.id, 200, redisPipeline);
this.globalEventService.publishAntennaStream(filter.antennaId, 'note', note);
}
}
redisPipeline.exec();
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
@bindThis
public async create(
ps: {
name: string;
src: AntennaSource;
keywords: string[][];
excludeKeywords: string[][];
users: string[];
caseSensitive: boolean;
localOnly?: boolean;
excludeBots?: boolean;
excludeNotesInSensitiveChannel?: boolean;
useRegex?: boolean;
withReplies: boolean;
withFile: boolean;
},
me: MiUser,
): Promise<MiAntenna> {
this.validateEmptyKeyWord(ps);
this.validateRegexPattern(ps);
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new AntennaService.TooManyAntennasError;
}
const now = new Date();
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
name: ps.name,
src: ps.src,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
useRegex: ps.useRegex,
withReplies: ps.withReplies,
withFile: ps.withFile,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
return antenna;
}
@bindThis
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false;
public async update(
ps: {
antennaId: string;
name?: string;
src?: AntennaSource;
keywords?: string[][];
excludeKeywords?: string[][];
users?: string[];
caseSensitive?: boolean;
localOnly?: boolean;
excludeBots?: boolean;
excludeNotesInSensitiveChannel?: boolean;
useRegex?: boolean;
withReplies?: boolean;
withFile?: boolean;
},
me: MiUser,
): Promise<MiAntenna> {
this.validateEmptyKeyWord({
keywords: ps.keywords ?? [],
excludeKeywords: ps.excludeKeywords ?? [],
});
if (antenna.excludeBots && noteUser.isBot) return false;
this.validateRegexPattern({
keywords: ps.keywords ?? [],
excludeKeywords: ps.excludeKeywords ?? [],
useRegex: ps.useRegex,
});
if (antenna.localOnly && noteUser.host != null) return false;
const antenna = await this.antennasRepository.findOneBy({
id: ps.antennaId,
userId: me.id,
});
if (antenna == null) {
throw new AntennaService.AntennaNotFoundError;
}
if (!antenna.withReplies && note.replyId != null) return false;
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
useRegex: ps.useRegex,
withReplies: ps.withReplies,
withFile: ps.withFile,
isActive: true,
lastUsedAt: new Date(),
});
if (note.visibility === 'specified') {
if (note.userId !== antenna.userId) {
if (note.visibleUserIds == null) return false;
if (!note.visibleUserIds.includes(antenna.userId)) return false;
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
return antenna;
}
@bindThis
private validateEmptyKeyWord(
ps: {
keywords: string[][];
excludeKeywords: string[][];
},
) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new AntennaService.EmptyKeyWordError;
}
}
@bindThis
private validateRegexPattern(
ps: {
keywords: string[][];
excludeKeywords: string[][];
useRegex?: boolean;
},
) {
if (!ps.useRegex) {
return;
}
// NOTE: 正規表現パターンの場合は2次元配列の2次元目0番地にパターンが格納されていることを前提にする
if (ps.keywords.length > 0 && ps.keywords.some(x => x.length !== 1)) {
throw new AntennaService.InvalidRegexPatternError();
}
if (ps.excludeKeywords.length > 0 && ps.excludeKeywords.some(x => x.length !== 1)) {
throw new AntennaService.InvalidRegexPatternError();
}
for (const keywords of [ps.keywords, ps.excludeKeywords]) {
for (const keyword of keywords) {
const regexp = keyword[0].match(/^\/(.+)\/(.*)$/);
if (!regexp) {
throw new AntennaService.InvalidRegexPatternError();
}
try {
new RE2(regexp[1], regexp[2]);
} catch (err) {
throw new AntennaService.InvalidRegexPatternError(err);
}
}
}
}
if (note.visibility === 'followers') {
const isFollowing = Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(antenna.userId), note.userId);
if (!isFollowing && antenna.userId !== note.userId) return false;
}
@bindThis
private isIncludeUser(antenna: MiAntenna, noteUser: { username: string; host: string | null; }): boolean {
const antennaUserAccounts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
if (antenna.userListId == null) return false;
const exists = await this.userListMembershipsRepository.exists({
where: {
userListId: antenna.userListId,
userId: note.userId,
},
});
if (!exists) return false;
} else if (antenna.src === 'users') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} else if (antenna.src === 'users_blacklist') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
}
const keywords = antenna.keywords
// Clean up
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
if (keywords.length > 0) {
if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = keywords.some(and =>
and.every(keyword =>
antenna.caseSensitive
? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()),
));
if (!matched) return false;
}
const excludeKeywords = antenna.excludeKeywords
// Clean up
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
if (excludeKeywords.length > 0) {
if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = excludeKeywords.some(and =>
and.every(keyword =>
antenna.caseSensitive
? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()),
));
if (matched) return false;
}
const noteUserAccount = this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase();
return antennaUserAccounts.includes(noteUserAccount);
}
@bindThis
public checkHitAntenna(
filter: AntennaFilter,
note: (MiNote | Packed<'Note'>),
noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; },
): boolean {
const antenna = filter.src;
if (antenna.excludeNotesInSensitiveChannel && note.channel?.isSensitive) return false;
if (antenna.excludeBots && noteUser.isBot) return false;
if (antenna.localOnly && noteUser.host != null) return false;
if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false;
}
// TODO: eval expression
if (antenna.src === 'users') {
if (!this.isIncludeUser(antenna, noteUser)) return false;
} else if (antenna.src === 'users_blacklist') {
if (this.isIncludeUser(antenna, noteUser)) return false;
}
return true;
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
return filter.testKeywords(_text);
}
@bindThis
public async getAntennas() {
if (!this.antennasFetched) {
this.antennas = await this.antennasRepository.findBy({
isActive: true,
});
this.antennasFetched = true;
private async getFilters() {
if (!this.filtersFetched) {
const antennas = await this.antennasRepository.findBy({ isActive: true });
this.filters = antennas.map(createAntennaFilter);
this.filtersFetched = true;
}
return this.antennas;
return this.filters;
}
@bindThis
@ -226,7 +442,7 @@ export class AntennaService implements OnApplicationShutdown {
// Get MiAntenna[] from cache and filter to select antennas with the src user is in the users list
const srcUserAcct = this.utilityService.getFullApAccount(src.username, src.host).toLowerCase();
const antennasToMigrate = (await this.getAntennas()).filter(antenna => {
const antennasToMigrate = (await this.getFilters()).map(it => it.src).filter(antenna => {
return antenna.users.some(user => {
const { username, host } = Acct.parse(user);
return this.utilityService.getFullApAccount(username, host).toLowerCase() === srcUserAcct;

View File

@ -39,6 +39,7 @@ export class AntennaEntityService {
caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly,
excludeBots: antenna.excludeBots,
useRegex: antenna.useRegex,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,

View File

@ -3,10 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { Serialized } from '@/types.js';
import { MiUser } from './User.js';
import { MiUserList } from './UserList.js';
import { id } from './util/id.js';
export const antennaSources = [
'all',
'users',
'users_blacklist',
// 'home', // TODO
// 'list', // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
] as const;
export type AntennaSource = typeof antennaSources[number];
@Entity('antenna')
export class MiAntenna {
@ -36,8 +46,8 @@ export class MiAntenna {
})
public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] })
public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
@Column('enum', { enum: antennaSources })
public src: AntennaSource;
@Column({
...id(),
@ -105,4 +115,20 @@ export class MiAntenna {
default: false,
})
public excludeNotesInSensitiveChannel: boolean;
@Column('boolean', {
default: false,
})
public useRegex: boolean;
public static deserialize(data: Serialized<MiAntenna>): MiAntenna {
return {
...data,
lastUsedAt: new Date(data.lastUsedAt),
// クエリビルダで明示的にSELECT/JOINしないかぎり設定されない値なのでnullにしておく
user: null,
// クエリビルダで明示的にSELECT/JOINしないかぎり設定されない値なのでnullにしておく
userList: null,
};
}
}

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { antennaSources } from '@/models/Antenna.js';
export const packedAntennaSchema = {
type: 'object',
properties: {
@ -47,7 +49,7 @@ export const packedAntennaSchema = {
src: {
type: 'string',
optional: false, nullable: false,
enum: ['home', 'all', 'users', 'list', 'users_blacklist'],
enum: antennaSources,
},
userListId: {
type: 'string',
@ -72,6 +74,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
useRegex: {
type: 'boolean',
optional: false, nullable: false,
},
excludeBots: {
type: 'boolean',
optional: false, nullable: false,

View File

@ -3,14 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { UserListsRepository, AntennasRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { Injectable } from '@nestjs/common';
import { AntennaService } from '@/core/AntennaService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { antennaSources } from '@/models/Antenna.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -40,6 +37,13 @@ export const meta = {
code: 'EMPTY_KEYWORD',
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
},
invalidRegexPattern: {
message: 'Invalid regex pattern.',
code: 'INVALID_REGEX_PATTERN',
id: 'b06d08f4-6434-5faa-0fdd-a2aaf85e9de7',
httpStatusCode: 400,
},
},
res: {
@ -53,93 +57,66 @@ export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: {
type: 'array', items: {
type: 'string',
src: { type: 'string', enum: antennaSources },
keywords: {
type: 'array',
items: {
type: 'array',
items: { type: 'string' },
},
} },
excludeKeywords: { type: 'array', items: {
type: 'array', items: {
type: 'string',
},
excludeKeywords: {
type: 'array',
items: {
type: 'array',
items: { type: 'string' },
},
} },
users: { type: 'array', items: {
type: 'string',
} },
},
users: {
type: 'array',
items: { type: 'string' },
},
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
excludeBots: { type: 'boolean' },
useRegex: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
excludeNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
required: [
'name',
'src',
'keywords',
'excludeKeywords',
'users',
'caseSensitive',
'withReplies',
'withFile',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private antennaEntityService: AntennaEntityService,
private roleService: RoleService,
private idService: IdService,
private globalEventService: GlobalEventService,
private readonly antennaEntityService: AntennaEntityService,
private readonly antennaService: AntennaService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
let userList;
if (ps.src === 'list' && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
try {
const antenna = await this.antennaService.create(ps, me);
return this.antennaEntityService.pack(antenna);
} catch (e) {
if (e instanceof AntennaService.EmptyKeyWordError) {
throw new ApiError(meta.errors.emptyKeyword);
} else if (e instanceof AntennaService.TooManyAntennasError) {
throw new ApiError(meta.errors.tooManyAntennas);
} else if (e instanceof AntennaService.InvalidRegexPatternError) {
throw new ApiError(meta.errors.invalidRegexPattern);
} else {
throw e;
}
}
const now = new Date();
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
name: ps.name,
src: ps.src,
userListId: userList ? userList.id : null,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
return await this.antennaEntityService.pack(antenna);
});
}
}

View File

@ -4,6 +4,8 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { AntennaService } from '@/core/AntennaService.js';
import { antennaSources } from '@/models/Antenna.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AntennasRepository, UserListsRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -38,6 +40,13 @@ export const meta = {
code: 'EMPTY_KEYWORD',
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
},
invalidRegexPattern: {
message: 'Invalid regex pattern.',
code: 'INVALID_REGEX_PATTERN',
id: 'dbb44ec3-5d15-508d-e6b2-71f3794c6a41',
httpStatusCode: 400,
},
},
res: {
@ -52,24 +61,29 @@ export const paramDef = {
properties: {
antennaId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: {
type: 'array', items: {
type: 'string',
src: { type: 'string', enum: antennaSources },
keywords: {
type: 'array',
items: {
type: 'array',
items: { type: 'string' },
},
} },
excludeKeywords: { type: 'array', items: {
type: 'array', items: {
type: 'string',
},
excludeKeywords: {
type: 'array',
items: {
type: 'array',
items: { type: 'string' },
},
} },
users: { type: 'array', items: {
type: 'string',
} },
},
users: {
type: 'array',
items: { type: 'string' },
},
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
excludeBots: { type: 'boolean' },
useRegex: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
excludeNotesInSensitiveChannel: { type: 'boolean' },
@ -80,64 +94,24 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private antennaEntityService: AntennaEntityService,
private globalEventService: GlobalEventService,
private readonly antennaEntityService: AntennaEntityService,
private readonly antennaService: AntennaService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
try {
const antenna = await this.antennaService.update(ps, me);
return this.antennaEntityService.pack(antenna);
} catch (e) {
if (e instanceof AntennaService.EmptyKeyWordError) {
throw new ApiError(meta.errors.emptyKeyword);
} else if (e instanceof AntennaService.AntennaNotFoundError) {
throw new ApiError(meta.errors.noSuchAntenna);
} else if (e instanceof AntennaService.InvalidRegexPatternError) {
throw new ApiError(meta.errors.invalidRegexPattern);
} else {
throw e;
}
}
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
id: ps.antennaId,
userId: me.id,
});
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
let userList;
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
}
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
isActive: true,
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
return await this.antennaEntityService.pack(antenna.id);
});
}
}

View File

@ -10,15 +10,15 @@ cd packages/backend/test-federation
First, you need to start servers by executing following commands:
```sh
bash ./setup.sh
docker compose up --scale tester=0
NODE_VERSION=22 docker compose up --scale tester=0
```
Then you can run all tests by a following command:
```sh
docker compose run --no-deps --rm tester
NODE_VERSION=22 docker compose run --no-deps --rm tester
```
For testing a specific file, run a following command:
```sh
docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
NODE_VERSION=22 docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
```

View File

@ -12,7 +12,7 @@ services:
retries: 20
misskey:
image: node:20
image: node:${NODE_VERSION}
env_file:
- ./.config/docker.env
environment:

View File

@ -16,7 +16,7 @@ services:
"
tester:
image: node:20
image: node:${NODE_VERSION}
depends_on:
a.test:
condition: service_healthy
@ -50,6 +50,10 @@ services:
source: ../jest.config.fed.cjs
target: /misskey/packages/backend/jest.config.fed.cjs
read_only: true
- type: bind
source: ../jest.js
target: /misskey/packages/backend/jest.js
read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
@ -85,7 +89,7 @@ services:
command: pnpm -F backend test:fed
daemon:
image: node:20
image: node:${NODE_VERSION}
depends_on:
redis.test:
condition: service_healthy

View File

@ -34,7 +34,7 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"uuid": "11.1.0",
"vite": "6.3.3",
"vite": "6.3.4",
"vue": "3.5.13"
},
"devDependencies": {

View File

@ -74,7 +74,7 @@
"typescript": "5.8.3",
"uuid": "11.1.0",
"v-code-diff": "1.13.1",
"vite": "6.3.3",
"vite": "6.3.4",
"vue": "3.5.13",
"vuedraggable": "next",
"wanakana": "5.3.1"

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick">
<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
<div ref="rootEl" :class="$style.root">
<div :class="$style.header">
<span :class="$style.icon">
@ -16,13 +16,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.title">{{ announcement.title }}</span>
</div>
<div :class="$style.text"><Mfm :text="announcement.text"/></div>
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
<div ref="bottomEl"></div>
<div :class="$style.footer">
<MkButton
primary
full
:disabled="!hasReachedBottom"
@click="ok"
>{{ hasReachedBottom ? i18n.ts.close : i18n.ts.scrollToClose }}</MkButton>
</div>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue';
import { onMounted, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -32,12 +40,12 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
const props = withDefaults(defineProps<{
const props = defineProps<{
announcement: Misskey.entities.Announcement;
}>(), {
});
}>();
const rootEl = useTemplateRef('rootEl');
const bottomEl = useTemplateRef('bottomEl');
const modal = useTemplateRef('modal');
async function ok() {
@ -72,7 +80,34 @@ function onBgClick() {
});
}
const hasReachedBottom = ref(false);
onMounted(() => {
if (bottomEl.value && rootEl.value) {
const bottomElRect = bottomEl.value.getBoundingClientRect();
const rootElRect = rootEl.value.getBoundingClientRect();
if (
bottomElRect.top >= rootElRect.top &&
bottomElRect.top <= (rootElRect.bottom - 66) // 66 75 * 0.9 (modal)
) {
hasReachedBottom.value = true;
return;
}
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
hasReachedBottom.value = true;
observer.disconnect();
}
}
}, {
root: rootEl.value,
rootMargin: '0px 0px -75px 0px',
});
observer.observe(bottomEl.value);
}
});
</script>
@ -80,9 +115,12 @@ onMounted(() => {
.root {
margin: auto;
position: relative;
padding: 32px;
padding: 32px 32px 0;
min-width: 320px;
max-width: 480px;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
@ -103,4 +141,14 @@ onMounted(() => {
.text {
margin: 1em 0;
}
.footer {
position: sticky;
bottom: 0;
left: -32px;
backdrop-filter: var(--MI-blur, blur(15px));
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
margin: 0 -32px;
padding: 24px 32px;
}
</style>

View File

@ -13,21 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="src">
<template #label>{{ i18n.ts.antennaSource }}</template>
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId">
<template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<MkTextarea v-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
</MkTextarea>
<MkSwitch v-model="excludeBots">{{ i18n.ts.antennaExcludeBots }}</MkSwitch>
<MkSwitch v-model="withReplies">{{ i18n.ts.withReplies }}</MkSwitch>
<MkSwitch v-model="useRegex">{{ i18n.ts.antennaUseRegex }}</MkSwitch>
<MkTextarea v-model="keywords">
<template #label>{{ i18n.ts.antennaKeywords }}</template>
<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
@ -88,6 +83,7 @@ const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
localOnly: false,
withFile: false,
excludeNotesInSensitiveChannel: false,
useRegex: false,
isActive: true,
hasUnreadNote: false,
notify: false,
@ -109,17 +105,51 @@ const caseSensitive = ref<boolean>(initialAntenna.caseSensitive);
const localOnly = ref<boolean>(initialAntenna.localOnly);
const excludeBots = ref<boolean>(initialAntenna.excludeBots);
const withReplies = ref<boolean>(initialAntenna.withReplies);
const useRegex = ref<boolean>(initialAntenna.useRegex);
const withFile = ref<boolean>(initialAntenna.withFile);
const excludeNotesInSensitiveChannel = ref<boolean>(initialAntenna.excludeNotesInSensitiveChannel);
const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => {
if (src.value === 'list' && userLists.value === null) {
userLists.value = await misskeyApi('users/lists/list');
}
});
async function saveAntenna() {
const _keywords: string[][] = [];
const _excludeKeywords: string[][] = [];
if (useRegex.value) {
function checkRegExError(words: string[], type: string) {
const errLineNumbers = words
.map((x, i) => {
try {
// RE2
new RegExp(x);
return null;
} catch {
return i + 1;
}
})
.filter(x => x != null);
if (errLineNumbers.length > 0) {
os.alert({
type: 'error',
text: i18n.tsx.antennaUseRegexError({ src: type, line: errLineNumbers.join(', ') }),
});
return false;
}
return true;
}
const keywordsDraft = keywords.value.trim().split('\n').map(x => x.trim()).filter(x => x.length > 0);
if (!checkRegExError(keywordsDraft, i18n.ts.antennaKeywords)) return;
const excludeKeywordsDraft = excludeKeywords.value.trim().split('\n').map(x => x.trim()).filter(x => x.length > 0);
if (!checkRegExError(excludeKeywordsDraft, i18n.ts.antennaExcludeKeywords)) return;
_keywords.push(...keywordsDraft.map(x => [x]));
_excludeKeywords.push(...excludeKeywordsDraft.map(x => [x]));
} else {
_keywords.push(...keywords.value.trim().split('\n').map(x => x.trim().split(' ')).filter(x => x.length > 0));
_excludeKeywords.push(...excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')).filter(x => x.length > 0));
}
const antennaData = {
name: name.value,
src: src.value,
@ -130,9 +160,10 @@ async function saveAntenna() {
excludeNotesInSensitiveChannel: excludeNotesInSensitiveChannel.value,
caseSensitive: caseSensitive.value,
localOnly: localOnly.value,
users: users.value.trim().split('\n').map(x => x.trim()),
keywords: keywords.value.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')),
useRegex: useRegex.value,
users: users.value.trim().split('\n').map(x => x.trim()).filter(x => x.length > 0),
keywords: _keywords,
excludeKeywords: _excludeKeywords,
};
if (initialAntenna.id == null) {

View File

@ -76,8 +76,8 @@ function unlockDownScroll() {
scrollEl.style.overscrollBehavior = 'contain';
}
function moveStart(event: PointerEvent) {
if (event.pointerType === 'mouse' && event.button !== 1) return;
function moveStartByMouse(event: MouseEvent) {
if (event.button !== 1) return;
if (isRefreshing.value) return;
const scrollPos = scrollEl!.scrollTop;
@ -88,27 +88,39 @@ function moveStart(event: PointerEvent) {
lockDownScroll();
// pull
window.document.body.setAttribute('inert', 'true');
event.preventDefault(); //
isPulling.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
// PointerEvent使TouchEventMouseEvent使
if (event.pointerType === 'mouse') {
window.addEventListener('mousemove', moving, { passive: true });
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', moving);
onPullRelease();
}, { passive: true, once: true });
} else {
window.addEventListener('touchmove', moving, { passive: true });
window.addEventListener('touchend', () => {
window.removeEventListener('touchmove', moving);
onPullRelease();
}, { passive: true, once: true });
window.addEventListener('mousemove', moving, { passive: true });
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', moving);
onPullRelease();
}, { passive: true, once: true });
}
function moveStartByTouch(event: TouchEvent) {
if (isRefreshing.value) return;
const scrollPos = scrollEl!.scrollTop;
if (scrollPos !== 0) {
unlockDownScroll();
return;
}
lockDownScroll();
isPulling.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
window.addEventListener('touchmove', moving, { passive: true });
window.addEventListener('touchend', () => {
window.removeEventListener('touchmove', moving);
onPullRelease();
}, { passive: true, once: true });
}
function moveBySystem(to: number): Promise<void> {
@ -148,7 +160,6 @@ async function closeContent() {
}
function onPullRelease() {
window.document.body.removeAttribute('inert');
startScreenY = null;
if (isPulledEnough.value) {
isPulledEnough.value = false;
@ -208,13 +219,15 @@ onMounted(() => {
if (rootEl.value == null) return;
scrollEl = getScrollContainer(rootEl.value);
lockDownScroll();
rootEl.value.addEventListener('pointerdown', moveStart, { passive: true });
rootEl.value.addEventListener('mousedown', moveStartByMouse, { passive: false }); // preventDefault
rootEl.value.addEventListener('touchstart', moveStartByTouch, { passive: true });
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
});
onUnmounted(() => {
unlockDownScroll();
if (rootEl.value) rootEl.value.removeEventListener('pointerdown', moveStart);
if (rootEl.value) rootEl.value.removeEventListener('mousedown', moveStartByMouse);
if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
});
</script>

View File

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTl :events="timeline">
<template #left="{ event }">
<div>
<MkAvatar :user="event.user" style="width: 24px; height: 24px;"/>
<MkAvatar :user="event.user" style="width: 26px; height: 26px;"/>
</div>
</template>
<template #right="{ event, timestamp, delta }">

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.5.0-alpha.0",
"version": "2025.5.0-alpha.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -4934,7 +4934,7 @@ export type components = {
keywords: string[][];
excludeKeywords: string[][];
/** @enum {string} */
src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
src: 'all' | 'users' | 'users_blacklist';
/** Format: id */
userListId: string | null;
users: string[];
@ -4942,6 +4942,7 @@ export type components = {
caseSensitive: boolean;
/** @default false */
localOnly: boolean;
useRegex: boolean;
/** @default false */
excludeBots: boolean;
/** @default false */
@ -11711,15 +11712,14 @@ export type operations = {
'application/json': {
name: string;
/** @enum {string} */
src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
/** Format: misskey:id */
userListId?: string | null;
src: 'all' | 'users' | 'users_blacklist';
keywords: string[][];
excludeKeywords: string[][];
users: string[];
caseSensitive: boolean;
localOnly?: boolean;
excludeBots?: boolean;
useRegex?: boolean;
withReplies: boolean;
withFile: boolean;
excludeNotesInSensitiveChannel?: boolean;
@ -11993,15 +11993,14 @@ export type operations = {
antennaId: string;
name?: string;
/** @enum {string} */
src?: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
/** Format: misskey:id */
userListId?: string | null;
src?: 'all' | 'users' | 'users_blacklist';
keywords?: string[][];
excludeKeywords?: string[][];
users?: string[];
caseSensitive?: boolean;
localOnly?: boolean;
excludeBots?: boolean;
useRegex?: boolean;
withReplies?: boolean;
withFile?: boolean;
excludeNotesInSensitiveChannel?: boolean;

View File

@ -736,7 +736,7 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.2.3
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@vue/compiler-sfc':
specifier: 3.5.13
version: 3.5.13
@ -873,8 +873,8 @@ importers:
specifier: 1.13.1
version: 1.13.1(vue@3.5.13(typescript@5.8.3))
vite:
specifier: 6.3.3
version: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
specifier: 6.3.4
version: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue:
specifier: 3.5.13
version: 3.5.13(typescript@5.8.3)
@ -926,7 +926,7 @@ importers:
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)
'@storybook/react-vite':
specifier: 8.6.12
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/test':
specifier: 8.6.12
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
@ -941,7 +941,7 @@ importers:
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.8.3))
'@storybook/vue3-vite':
specifier: 8.6.12
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@testing-library/vue':
specifier: 8.1.0
version: 8.1.0(@vue/compiler-sfc@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
@ -1094,7 +1094,7 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.2.3
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@vue/compiler-sfc':
specifier: 3.5.13
version: 3.5.13
@ -1147,8 +1147,8 @@ importers:
specifier: 11.1.0
version: 11.1.0
vite:
specifier: 6.3.3
version: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
specifier: 6.3.4
version: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue:
specifier: 3.5.13
version: 3.5.13(typescript@5.8.3)
@ -4114,6 +4114,7 @@ packages:
'@swc/core@1.11.22':
resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==}
engines: {node: '>=10'}
deprecated: It has a bug. See https://github.com/swc-project/swc/issues/10413
peerDependencies:
'@swc/helpers': '>=0.5.17'
peerDependenciesMeta:
@ -10624,8 +10625,8 @@ packages:
vite-plugin-turbosnap@1.0.3:
resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==}
vite@6.3.3:
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
vite@6.3.4:
resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
@ -12619,12 +12620,12 @@ snapshots:
'@types/yargs': 17.0.19
chalk: 4.1.2
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
glob: 10.4.5
magic-string: 0.27.0
react-docgen-typescript: 2.2.2(typescript@5.8.3)
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
optionalDependencies:
typescript: 5.8.3
@ -14151,13 +14152,13 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@storybook/builder-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@storybook/builder-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@storybook/csf-plugin': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
browser-assert: 1.2.1
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
ts-dedent: 2.2.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
'@storybook/components@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))':
dependencies:
@ -14220,11 +14221,11 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
'@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@rollup/pluginutils': 5.1.4(rollup@4.40.0)
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)
find-up: 5.0.0
magic-string: 0.30.17
@ -14234,7 +14235,7 @@ snapshots:
resolve: 1.22.8
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
tsconfig-paths: 4.2.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
optionalDependencies:
'@storybook/test': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
transitivePeerDependencies:
@ -14283,15 +14284,15 @@ snapshots:
dependencies:
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
'@storybook/vue3-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
'@storybook/vue3-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
dependencies:
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/vue3': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.8.3))
find-package-json: 1.2.0
magic-string: 0.30.17
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
typescript: 5.8.3
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue-component-meta: 2.0.16(typescript@5.8.3)
vue-docgen-api: 4.75.1(vue@3.5.13(typescript@5.8.3))
transitivePeerDependencies:
@ -14996,9 +14997,9 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
'@vitejs/plugin-vue@5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
'@vitejs/plugin-vue@5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
dependencies:
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue: 3.5.13(typescript@5.8.3)
'@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
@ -15033,14 +15034,14 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@vitest/mocker@3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@vitest/spy': 3.1.2
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.7.5(@types/node@22.15.2)(typescript@5.8.3)
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
'@vitest/pretty-format@2.0.5':
dependencies:
@ -22120,7 +22121,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -22137,7 +22138,7 @@ snapshots:
vite-plugin-turbosnap@1.0.3: {}
vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
dependencies:
esbuild: 0.25.3
fdir: 6.4.4(picomatch@4.0.2)
@ -22159,7 +22160,7 @@ snapshots:
vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
dependencies:
'@vitest/expect': 3.1.2
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@vitest/pretty-format': 3.1.2
'@vitest/runner': 3.1.2
'@vitest/snapshot': 3.1.2
@ -22176,7 +22177,7 @@ snapshots:
tinyglobby: 0.2.13
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite-node: 3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
why-is-node-running: 2.3.0
optionalDependencies:

View File

@ -16,7 +16,7 @@
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.3",
"vite": "6.3.4",
"vite-node": "3.1.2",
"vitest": "3.1.2"
}
@ -2785,9 +2785,9 @@
}
},
"node_modules/vite": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
"integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -17,7 +17,7 @@
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.3",
"vite": "6.3.4",
"vite-node": "3.1.2",
"vitest": "3.1.2"
}