Compare commits
17 Commits
ec19429303
...
a67795a195
Author | SHA1 | Date |
---|---|---|
|
a67795a195 | |
|
d522d1bf26 | |
|
080276e3e7 | |
|
619bb2214e | |
|
c23f2ff900 | |
|
14d6734cb1 | |
|
3bdb1dd558 | |
|
e75d749784 | |
|
42a2ed8b67 | |
|
a5fa9a2cef | |
|
5b6ace0b8a | |
|
d681dc42e8 | |
|
4c40d0a809 | |
|
fae4147904 | |
|
dfa66e6e2a | |
|
08b8bf94ef | |
|
7a8ab424d4 |
|
@ -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"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
20.10.0
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.11.0
|
||||
22.15.0
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
|
|
@ -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: "まだメッセージはありません"
|
||||
|
|
|
@ -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 發送訊息"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.5.0-alpha.0",
|
||||
"version": "2025.5.0-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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' });
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -12,7 +12,7 @@ services:
|
|||
retries: 20
|
||||
|
||||
misskey:
|
||||
image: node:20
|
||||
image: node:${NODE_VERSION}
|
||||
env_file:
|
||||
- ./.config/docker.env
|
||||
environment:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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を使うとなんか挙動がおかしいので、TouchEventとMouseEventを使い分ける
|
||||
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>
|
||||
|
|
|
@ -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 }">
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue