Compare commits

..

1 Commits

Author SHA1 Message Date
Kisaragi
fcb9f8bef4 docs: FEP-67ff 2025-02-08 13:35:10 +09:00
173 changed files with 7076 additions and 8004 deletions

View File

@@ -220,10 +220,5 @@ allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -235,11 +235,6 @@ signToActivityPubGet: true
# '127.0.0.1/32'
#]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -334,11 +334,6 @@ signToActivityPubGet: true
# '127.0.0.1/32'
#]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -7,9 +7,7 @@
"ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0"
},
"ghcr.io/devcontainers-extra/features/corepack:1": {
"version": "0.31.0"
}
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},
"forwardPorts": [3000],
"postCreateCommand": "/bin/bash .devcontainer/init.sh",

View File

@@ -9,7 +9,7 @@ updates:
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 0
open-pull-requests-limit: 100
# Add only the root, not each workspace item
# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027
@@ -17,7 +17,7 @@ updates:
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 0
open-pull-requests-limit: 10
# List dependencies required to be updated together, sharing the same version numbers.
# Those who simply have the common owner (e.g. @fastify) don't need to be listed.
groups:

View File

@@ -20,12 +20,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.1
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ jobs:
DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"

View File

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

View File

@@ -36,12 +36,12 @@ jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -67,19 +67,19 @@ jobs:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Restore eslint cache
uses: actions/cache@v4.2.1
uses: actions/cache@v4.2.0
with:
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
@@ -97,12 +97,12 @@ jobs:
- sw
- misskey-js
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -18,12 +18,12 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

36
.github/workflows/ok-to-test.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
name: Ok To Test
on:
issue_comment:
types: [created]
jobs:
ok-to-test:
runs-on: ubuntu-latest
# Only run for PRs, not issue comments
if: ${{ github.event.issue.pull_request }}
steps:
# Generate a GitHub App installation access token from an App ID and private key
# To create a new GitHub App:
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
# See app.yml for an example app manifest
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
with:
token: ${{ env.TOKEN }} # GitHub App installation access token
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
reaction-token: ${{ secrets.GITHUB_TOKEN }}
issue-type: pull-request
commands: deploy
named-args: true
permission: write

View File

@@ -23,13 +23,13 @@ jobs:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

92
.github/workflows/pr-preview-deploy.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
# Run secret-dependent integration tests only after /deploy approval
on:
repository_dispatch:
types: [deploy-command]
name: Deploy preview environment
jobs:
# Repo owner has commented /deploy on a (fork-based) pull request
deploy-preview-environment:
runs-on: ubuntu-latest
if:
github.event.client_payload.slash_command.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
steps:
- uses: actions/github-script@v7.0.1
id: check-id
env:
number: ${{ github.event.client_payload.pull_request.number }}
job: ${{ github.job }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-encoding: string
script: |
const { data: pull } = await github.rest.pulls.get({
...context.repo,
pull_number: process.env.number
});
const ref = pull.head.sha;
const { data: checks } = await github.rest.checks.listForRef({
...context.repo,
ref
});
const check = checks.check_runs.filter(c => c.name === process.env.job);
return check[0].id;
- uses: actions/github-script@v7.0.1
env:
check_id: ${{ steps.check-id.outputs.result }}
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.checks.update({
...context.repo,
check_run_id: process.env.check_id,
status: 'in_progress',
details_url: process.env.details_url
});
# Check out merge commit
- name: Fork based /deploy checkout
uses: actions/checkout@v4.1.1
with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
# <insert integration tests needing secrets>
- name: Context
uses: okteto/context@latest
with:
token: ${{ secrets.OKTETO_TOKEN }}
- name: Deploy preview environment
uses: ikuradon/deploy-preview@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
timeout: 15m
# Update check run called "integration-fork"
- uses: actions/github-script@v7.0.1
id: update-check-run
if: ${{ always() }}
env:
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
conclusion: ${{ job.status }}
check_id: ${{ steps.check-id.outputs.result }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: result } = await github.rest.checks.update({
...context.repo,
check_run_id: process.env.check_id,
status: 'completed',
conclusion: process.env.conclusion
});
return result;

View File

@@ -0,0 +1,54 @@
# file: .github/workflows/preview-closed.yaml
on:
pull_request:
types:
- closed
name: Destroy preview environment
jobs:
destroy-preview-environment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7.0.1
id: check-conclusion
env:
number: ${{ github.event.number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-encoding: string
script: |
const { data: pull } = await github.rest.pulls.get({
...context.repo,
pull_number: process.env.number
});
const ref = pull.head.sha;
const { data: checks } = await github.rest.checks.listForRef({
...context.repo,
ref
});
const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
if (check.length === 0) {
return;
}
const { data: result } = await github.rest.checks.get({
...context.repo,
check_run_id: check[0].id,
});
return result.conclusion;
- name: Context
if: steps.check-conclusion.outputs.result == 'success'
uses: okteto/context@latest
with:
token: ${{ secrets.OKTETO_TOKEN }}
- name: Destroy preview environment
if: steps.check-conclusion.outputs.result == 'success'
uses: okteto/destroy-preview@latest
with:
name: pr-${{ github.event.number }}-syuilo

View File

@@ -26,12 +26,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
@@ -46,7 +46,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -45,7 +45,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
@@ -66,7 +66,7 @@ jobs:
fi
done
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -108,13 +108,13 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

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

View File

@@ -36,13 +36,13 @@ jobs:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -86,7 +86,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150
@@ -98,7 +98,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -31,12 +31,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.1
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -21,13 +21,13 @@ jobs:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -25,13 +25,13 @@ jobs:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -1,33 +1,15 @@
## 2025.2.1
## Unreleased
### General
- Feat: アクセストークン発行時に通知するように
- Feat: 実験的なGoogleAnalyticsサポートを追加
- 依存関係の更新
-
### Client
- Feat: 投稿フォームで画像をプレビュー可能に
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
- Enhance: クライアントエラー画面の多言語対応
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 )
- Enhance: リアクションする際に確認ダイアログを表示できるように
- Enhance: CWの注釈で入力済みの文字数を表示
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378`
- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 )
- Fix: CWの注釈が100文字を超えている場合、ート投稿ボタンを非アクティブに
### Server
- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect)
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
- Fix: フォロワーではないユーザーにリートもしくは返信された場合にートのDeleteアクティビティが送られていない問題を修正
-
## 2025.2.0

20
FEDERATION.md Normal file
View File

@@ -0,0 +1,20 @@
# Federation
## Supported federation protocols and standards
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
- [WebFinger](https://webfinger.net/)
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
- [NodeInfo](https://nodeinfo.diaspora.software/)
## Supported FEPs
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
## ActivityPub
This section is documented on [misskey-hub.net](https://misskey-hub.net/ns).
## Additional documentation
* https://misskey-hub.net/ns

70
locales/index.d.ts vendored
View File

@@ -5254,14 +5254,6 @@ export interface Locale extends ILocale {
* このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
*/
"federationDisabled": string;
/**
* リアクションする際に確認する
*/
"confirmOnReact": string;
/**
* " {emoji} " をリアクションしますか?
*/
"reactAreYouSure": ParameterizedString<"emoji">;
"_accountSettings": {
/**
* コンテンツの表示にログインを必須にする
@@ -9480,14 +9472,6 @@ export interface Locale extends ILocale {
* ログインがありました
*/
"login": string;
/**
* アクセストークンが作成されました
*/
"createToken": string;
/**
* 心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。
*/
"createTokenDescription": ParameterizedString<"text">;
"_types": {
/**
* すべて
@@ -10896,7 +10880,13 @@ export interface Locale extends ILocale {
*/
"title": string;
/**
* このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。
* このサーバーと通信することはできましたが、得られたデータが不正なものでした。
*/
"description": string;
};
"_responseInvalidIdHostNotMatch": {
/**
* 入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。
*/
"description": string;
};
@@ -10954,52 +10944,6 @@ export interface Locale extends ILocale {
};
};
};
"_bootErrors": {
/**
* 読み込みに失敗しました
*/
"title": string;
/**
* 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。
*/
"serverError": string;
/**
* 以下を行うと解決する可能性があります。
*/
"solution": string;
/**
* ブラウザおよびOSを最新バージョンに更新する
*/
"solution1": string;
/**
* アドブロッカーを無効にする
*/
"solution2": string;
/**
* ブラウザのキャッシュをクリアする
*/
"solution3": string;
/**
* (Tor Browser) dom.webaudio.enabledをtrueに設定する
*/
"solution4": string;
/**
* その他のオプション
*/
"otherOption": string;
/**
* クライアント設定とキャッシュを削除
*/
"otherOption1": string;
/**
* 簡易クライアントを起動
*/
"otherOption2": string;
/**
* 修復ツールを起動
*/
"otherOption3": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@@ -1309,8 +1309,6 @@ availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@@ -2502,8 +2500,6 @@ _notification:
flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました"
login: "ログインがありました"
createToken: "アクセストークンが作成されました"
createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。"
_types:
all: "すべて"
@@ -2911,7 +2907,9 @@ _remoteLookupErrors:
description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
_responseInvalid:
title: "レスポンスが不正です"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
_responseInvalidIdHostNotMatch:
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
_noSuchObject:
title: "見つかりません"
description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
@@ -2929,16 +2927,3 @@ _captcha:
_unknown:
title: "CAPTCHAエラー"
text: "想定外のエラーが発生しました。"
_bootErrors:
title: "読み込みに失敗しました"
serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
solution: "以下を行うと解決する可能性があります。"
solution1: "ブラウザおよびOSを最新バージョンに更新する"
solution2: "アドブロッカーを無効にする"
solution3: "ブラウザのキャッシュをクリアする"
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
otherOption: "その他のオプション"
otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.2.1-beta.1",
"version": "2025.2.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@9.15.4",
"packageManager": "pnpm@9.6.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@@ -47,35 +47,35 @@
"cleanall": "pnpm clean-all"
},
"resolutions": {
"chokidar": "3.6.0",
"chokidar": "3.5.3",
"lodash": "4.17.21"
},
"dependencies": {
"cssnano": "7.0.6",
"cssnano": "6.1.2",
"execa": "8.0.1",
"fast-glob": "3.3.3",
"fast-glob": "3.3.2",
"ignore-walk": "6.0.5",
"js-yaml": "4.1.0",
"postcss": "8.5.2",
"postcss": "8.4.49",
"tar": "6.2.1",
"terser": "5.39.0",
"typescript": "5.7.3",
"esbuild": "0.25.0",
"glob": "11.0.1"
"terser": "5.36.0",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "22.13.4",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"cross-env": "7.0.3",
"cypress": "14.0.3",
"eslint": "9.20.1",
"globals": "15.15.0",
"cypress": "13.15.2",
"eslint": "9.14.0",
"globals": "15.12.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.10"
"start-server-and-test": "2.0.8"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
"@tensorflow/tfjs-core": "4.4.0"
}
}

View File

@@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class GoogleAnalytics1739006797620 {
name = 'GoogleAnalytics1739006797620'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsMeasurementId" character varying(64)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsMeasurementId"`);
}
}

View File

@@ -37,20 +37,20 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.10.16",
"@swc/core-darwin-x64": "1.10.16",
"@swc/core-darwin-arm64": "1.3.56",
"@swc/core-darwin-x64": "1.3.56",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.10.16",
"@swc/core-linux-arm64-gnu": "1.10.16",
"@swc/core-linux-arm64-musl": "1.10.16",
"@swc/core-linux-x64-gnu": "1.10.16",
"@swc/core-linux-x64-musl": "1.10.16",
"@swc/core-win32-arm64-msvc": "1.10.16",
"@swc/core-win32-ia32-msvc": "1.10.16",
"@swc/core-win32-x64-msvc": "1.10.16",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
"@swc/core-linux-arm-gnueabihf": "1.3.56",
"@swc/core-linux-arm64-gnu": "1.3.56",
"@swc/core-linux-arm64-musl": "1.3.56",
"@swc/core-linux-x64-gnu": "1.3.56",
"@swc/core-linux-x64-musl": "1.3.56",
"@swc/core-win32-arm64-msvc": "1.3.56",
"@swc/core-win32-ia32-msvc": "1.3.56",
"@swc/core-win32-x64-msvc": "1.3.56",
"@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.4.0",
"bufferutil": "4.0.7",
"slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10",
"slacc-darwin-arm64": "0.0.10",
@@ -64,37 +64,37 @@
"slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "6.0.5"
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.749.0",
"@aws-sdk/lib-storage": "3.749.0",
"@bull-board/api": "6.7.7",
"@bull-board/fastify": "6.7.7",
"@bull-board/ui": "6.7.7",
"@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "6.5.0",
"@bull-board/fastify": "6.5.0",
"@bull-board/ui": "6.5.0",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.0.2",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.0.3",
"@fastify/static": "8.1.0",
"@fastify/view": "10.0.2",
"@fastify/accepts": "5.0.1",
"@fastify/cookie": "11.0.1",
"@fastify/cors": "10.0.1",
"@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.1",
"@fastify/multipart": "9.0.1",
"@fastify/static": "8.0.2",
"@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.0",
"@napi-rs/canvas": "0.1.67",
"@nestjs/common": "11.0.9",
"@nestjs/core": "11.0.9",
"@nestjs/testing": "11.0.9",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.56",
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7",
"@nestjs/testing": "10.4.7",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.55.0",
"@sentry/profiling-node": "8.55.0",
"@simplewebauthn/server": "12.0.0",
"@sinonjs/fake-timers": "11.3.1",
"@sentry/node": "8.38.0",
"@sentry/profiling-node": "8.38.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.6.0",
"@swc/core": "1.10.16",
"@swc/cli": "0.3.12",
"@swc/core": "1.9.2",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.17.1",
@@ -103,10 +103,10 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.41.1",
"bullmq": "5.26.1",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.4.1",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "3.6.0",
"cli-highlight": "2.1.11",
@@ -114,46 +114,46 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "5.2.1",
"fastify": "5.0.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.2",
"got": "14.4.6",
"happy-dom": "16.8.1",
"form-data": "4.0.1",
"got": "14.4.4",
"happy-dom": "15.11.4",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.5.0",
"ioredis": "5.4.1",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
"js-yaml": "4.1.0",
"jsdom": "26.0.0",
"jsdom": "24.1.1",
"json5": "2.2.3",
"jsonld": "8.3.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"juice": "11.0.0",
"meilisearch": "0.48.2",
"meilisearch": "0.45.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1",
"nanoid": "5.1.0",
"nanoid": "5.0.8",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.10.0",
"nodemailer": "6.9.16",
"nsfwjs": "4.2.0",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.3.6",
"otpauth": "9.3.4",
"parse5": "7.2.1",
"pg": "8.13.3",
"pg": "8.13.1",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@@ -167,19 +167,19 @@
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"sanitize-html": "2.14.0",
"secure-json-parse": "3.0.2",
"sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.25.11",
"systeminformation": "5.23.5",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.7.3",
"typescript": "5.6.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
@@ -188,8 +188,8 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.15",
"@simplewebauthn/types": "12.0.0",
"@nestjs/platform-express": "10.4.7",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.37",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.3",
@@ -204,15 +204,15 @@
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.15",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.13.4",
"@types/nodemailer": "6.4.17",
"@types/node": "22.9.0",
"@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.11",
"@types/pg": "8.11.10",
"@types/pug": "2.0.10",
"@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5",
@@ -226,18 +226,18 @@
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.5.14",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"aws-sdk-client-mock": "4.1.0",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.30.0",
"execa": "8.0.1",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.9",
"pid-port": "1.0.2",
"nodemon": "3.1.7",
"pid-port": "1.0.0",
"simple-oauth2": "5.1.0"
}
}

View File

@@ -73,7 +73,6 @@ type Source = {
proxyBypassHosts?: string[];
allowedPrivateNetworks?: string[];
disallowExternalApRedirect?: boolean;
maxFileSize?: number;
@@ -150,7 +149,6 @@ export type Config = {
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
disallowExternalApRedirect: boolean;
maxFileSize: number;
clusterLimit: number | undefined;
id: string;
@@ -289,7 +287,6 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
disallowExternalApRedirect: config.disallowExternalApRedirect ?? false,
maxFileSize: config.maxFileSize ?? 262144000,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,

View File

@@ -43,7 +43,7 @@ export type CaptchaSetting = {
siteKey: string | null;
secretKey: string | null;
}
};
}
export class CaptchaError extends Error {
public readonly code: CaptchaErrorCode;
@@ -59,11 +59,11 @@ export class CaptchaError extends Error {
export type CaptchaSaveSuccess = {
success: true;
};
}
export type CaptchaSaveFailure = {
success: false;
error: CaptchaError;
};
}
export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
type CaptchaResponse = {

View File

@@ -173,8 +173,7 @@ export class DriveService {
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
// for original
const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : '';
const key = `${prefix}${randomUUID()}${ext}`;
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`;
// for alts
@@ -191,7 +190,7 @@ export class DriveService {
];
if (alts.webpublic) {
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@@ -199,7 +198,7 @@ export class DriveService {
}
if (alts.thumbnail) {
thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);

View File

@@ -164,13 +164,6 @@ export class EmailService {
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> {
if (!this.utilityService.validateEmailFormat(emailAddress)) {
return {
available: false,
reason: 'format',
};
}
const exist = await this.userProfilesRepository.countBy({
emailVerified: true,
email: emailAddress,

View File

@@ -9,7 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
export type FanoutTimelineName = (
export type FanoutTimelineName =
// home timeline
| `homeTimeline:${string}`
| `homeTimelineWithFiles:${string}` // only notes with files are included
@@ -37,7 +37,6 @@ export type FanoutTimelineName = (
// role timelines
| `roleTimeline:${string}` // any notes are included
);
@Injectable()
export class FanoutTimelineService {

View File

@@ -211,7 +211,7 @@ type SerializedAll<T> = {
type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K];
};
}
export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };

View File

@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
@@ -215,7 +215,7 @@ export class HttpRequestService {
}
@bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObject> {
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
const res = await this.send(url, {
method: 'GET',
headers: {
@@ -232,7 +232,7 @@ export class HttpRequestService {
const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
assertActivityMatchesUrls(activity, [finalUrl]);
return activity;
}

View File

@@ -492,8 +492,7 @@ export class MfmService {
appendChildren(nodes, body);
// Remove the unnecessary namespace
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
const serialized = new XMLSerializer().serializeToString(body);
happyDOM.close().catch(err => {});

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets, In, IsNull, Not } from 'typeorm';
import { Brackets, In } from 'typeorm';
import { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
@@ -189,27 +189,13 @@ export class NoteDeleteService {
}) as MiRemoteUser[];
}
@bindThis
private async getRenotedOrRepliedRemoteUsers(note: MiNote) {
const query = this.notesRepository.createQueryBuilder('note')
.leftJoinAndSelect('note.user', 'user')
.where(new Brackets(qb => {
qb.orWhere('note.renoteId = :renoteId', { renoteId: note.id });
qb.orWhere('note.replyId = :replyId', { replyId: note.id });
}))
.andWhere({ userHost: Not(IsNull()) });
const notes = await query.getMany() as (MiNote & { user: MiRemoteUser })[];
const remoteUsers = notes.map(({ user }) => user);
return remoteUsers;
}
@bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content);
this.apDeliverManagerService.deliverToUsers(user, content, [
...await this.getMentionedRemoteUsers(note),
...await this.getRenotedOrRepliedRemoteUsers(note),
]);
const remoteUsers = await this.getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
}
}
}

View File

@@ -220,7 +220,7 @@ export class SearchService {
.leftJoinAndSelect('renote.user', 'renoteUser');
if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
query.andWhere('note.text &@~ :q', { q });
query.andWhere('note.text &@ :q', { q });
} else {
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` });
}

View File

@@ -38,14 +38,6 @@ export class UtilityService {
return this.punyHost(uri) === this.toPuny(this.config.host);
}
// メールアドレスのバリデーションを行う
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
@bindThis
public validateEmailFormat(email: string): boolean {
const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return regexp.test(email);
}
@bindThis
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
if (host == null) return false;

View File

@@ -127,11 +127,11 @@ export class WebAuthnService {
const { registrationInfo } = verification;
return {
credentialID: registrationInfo.credential.id,
credentialPublicKey: registrationInfo.credential.publicKey,
credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credentialPublicKey,
attestationObject: registrationInfo.attestationObject,
fmt: registrationInfo.fmt,
counter: registrationInfo.credential.counter,
counter: registrationInfo.counter,
userVerified: registrationInfo.userVerified,
credentialDeviceType: registrationInfo.credentialDeviceType,
credentialBackedUp: registrationInfo.credentialBackedUp,
@@ -212,9 +212,9 @@ export class WebAuthnService {
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
credential: {
id: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'),
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},
@@ -292,9 +292,9 @@ export class WebAuthnService {
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
credential: {
id: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'),
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},

View File

@@ -196,25 +196,6 @@ export class ApDeliverManagerService {
await manager.execute();
}
/**
* Deliver activity to users
* @param actor
* @param activity Activity
* @param targets Target users
*/
@bindThis
public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
for (const to of targets) manager.addDirectRecipe(to);
await manager.execute();
}
@bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager(

View File

@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js';
type Request = {
@@ -185,7 +185,7 @@ export class ApRequestService {
* @param url URL to fetch
*/
@bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict, followAlternate?: boolean): Promise<unknown> {
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@@ -243,7 +243,7 @@ export class ApRequestService {
if (alternate) {
const href = alternate.getAttribute('href');
if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
return await this.signedGet(href, user, allowSoftfail, false);
return await this.signedGet(href, user, false);
}
}
} catch (e) {
@@ -258,7 +258,7 @@ export class ApRequestService {
const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
assertActivityMatchesUrls(activity, [finalUrl]);
return activity;
}

View File

@@ -21,7 +21,6 @@ import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
export class Resolver {
private history: Set<string>;
@@ -73,7 +72,7 @@ export class Resolver {
}
@bindThis
public async resolve(value: string | IObject, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObject> {
public async resolve(value: string | IObject): Promise<IObject> {
if (typeof value !== 'string') {
return value;
}
@@ -109,8 +108,8 @@ export class Resolver {
}
const object = (this.user
? await this.apRequestService.signedGet(value, this.user, allowSoftfail) as IObject
: await this.httpRequestService.getActivityJson(value, undefined, allowSoftfail)) as IObject;
? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getActivityJson(value)) as IObject;
if (
Array.isArray(object['@context']) ?
@@ -120,6 +119,18 @@ export class Resolver {
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
}
// HttpRequestService / ApRequestService have already checked that
// `object.id` or `object.url` matches the URL used to fetch the
// object after redirects; here we double-check that no redirects
// bounced between hosts
if (object.id == null) {
throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', 'invalid AP object: missing id');
}
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`);
}
return object;
}

View File

@@ -4,124 +4,18 @@
*/
import type { IObject } from '../type.js';
export enum FetchAllowSoftFailMask {
// Allow no softfail flags
Strict = 0,
// The values in tuple (requestUrl, finalUrl, objectId) are not all identical
//
// This condition is common for user-initiated lookups but should not be allowed in federation loop
//
// Allow variations:
// good example: https://alice.example.com/@user -> https://alice.example.com/user/:userId
// problematic example: https://alice.example.com/redirect?url=https://bad.example.com/ -> https://bad.example.com/ -> https://alice.example.com/somethingElse
NonCanonicalId = 1 << 0,
// Allow the final object to be at most one subdomain deeper than the request URL, similar to SPF relaxed alignment
//
// Currently no code path allows this flag to be set, but is kept in case of future use as some niche deployments do this, and we provide a pre-reviewed mechanism to opt-in.
//
// Allow variations:
// good example: https://example.com/@user -> https://activitypub.example.com/@user { id: 'https://activitypub.example.com/@user' }
// problematic example: https://example.com/@user -> https://untrusted.example.com/@user { id: 'https://untrusted.example.com/@user' }
MisalignedOrigin = 1 << 1,
// The requested URL has a different host than the returned object ID, although the final URL is still consistent with the object ID
//
// This condition is common for user-initiated lookups using an intermediate host but should not be allowed in federation loops
//
// Allow variations:
// good example: https://alice.example.com/@user@bob.example.com -> https://bob.example.com/@user { id: 'https://bob.example.com/@user' }
// problematic example: https://alice.example.com/definitelyAlice -> https://bob.example.com/@somebodyElse { id: 'https://bob.example.com/@somebodyElse' }
CrossOrigin = 1 << 2 | MisalignedOrigin,
// Allow all softfail flags
//
// do not use this flag on released code
Any = ~0,
}
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
const hosts = urls.map(it => new URL(it).host);
/**
* Fuzz match on whether the candidate host has authority over the request host
*
* @param requestHost The host of the requested resources
* @param candidateHost The host of final response
* @returns Whether the candidate host has authority over the request host, or if a soft fail is required for a match
*/
function hostFuzzyMatch(requestHost: string, candidateHost: string): FetchAllowSoftFailMask {
const requestFqdn = requestHost.endsWith('.') ? requestHost : `${requestHost}.`;
const candidateFqdn = candidateHost.endsWith('.') ? candidateHost : `${candidateHost}.`;
const idOk = activity.id !== undefined && hosts.includes(new URL(activity.id).host);
if (requestFqdn === candidateFqdn) {
return FetchAllowSoftFailMask.Strict;
}
// technically `activity.url` could be an `ApObject = IObject |
// string | (IObject | string)[]`, but if it's a complicated thing
// and the `activity.id` doesn't match, I think we're fine
// rejecting the activity
const urlOk = typeof(activity.url) === 'string' && hosts.includes(new URL(activity.url).host);
// allow only one case where candidateHost is a first-level subdomain of requestHost
const requestDnsDepth = requestFqdn.split('.').length;
const candidateDnsDepth = candidateFqdn.split('.').length;
if ((candidateDnsDepth - requestDnsDepth) !== 1) {
return FetchAllowSoftFailMask.CrossOrigin;
}
if (`.${candidateHost}`.endsWith(`.${requestHost}`)) {
return FetchAllowSoftFailMask.MisalignedOrigin;
}
return FetchAllowSoftFailMask.CrossOrigin;
}
// normalize host names by removing www. prefix
function normalizeSynonymousSubdomain(url: URL | string): URL {
const urlParsed = url instanceof URL ? url : new URL(url);
const host = urlParsed.host;
const normalizedHost = host.replace(/^www\./, '');
return new URL(urlParsed.toString().replace(host, normalizedHost));
}
export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
// must have a unique identifier to verify authority
if (!activity.id) {
throw new Error('bad Activity: missing id field');
}
let softfail = 0;
// if the flag is allowed, set the flag on return otherwise throw
const requireSoftfail = (needed: FetchAllowSoftFailMask, message: string) => {
if ((allowSoftfail & needed) !== needed) {
throw new Error(message);
}
softfail |= needed;
};
const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl);
const idParsed = normalizeSynonymousSubdomain(activity.id);
const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it));
const requestUrlSecure = requestUrlParsed.protocol === 'https:';
const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:');
if (requestUrlSecure && !finalUrlSecure) {
throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`);
}
// Compare final URL to the ID
if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) {
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`);
// at lease host need to match exactly (ActivityPub requirement)
if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) {
throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`);
if (!idOk && !urlOk) {
throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`);
}
}
// Compare request URL to the ID
if (!requestUrlParsed.href.includes(idParsed.href)) {
requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`);
// if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID)
const hostResult = hostFuzzyMatch(requestUrlParsed.host, idParsed.host);
requireSoftfail(hostResult, `bad Activity: id(${activity.id}) is valid but is not the same origin as request url(${requestUrlParsed.toString()})`);
}
return softfail;
}

View File

@@ -97,7 +97,6 @@ export class MetaEntityService {
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha,
googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',

View File

@@ -57,14 +57,12 @@ const ajv = new Ajv();
function isLocalUser(user: MiUser): user is MiLocalUser;
function isLocalUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: null; });
function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean {
return user.host == null;
}
function isRemoteUser(user: MiUser): user is MiRemoteUser;
function isRemoteUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: string; });
function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user);
}
@@ -80,7 +78,7 @@ export type UserRelation = {
isBlocked: boolean
isMuted: boolean
isRenoteMuted: boolean
};
}
@Injectable()
export class UserEntityService implements OnModuleInit {

View File

@@ -143,7 +143,7 @@ type OfSchema = {
readonly anyOf?: ReadonlyArray<Schema>;
readonly oneOf?: ReadonlyArray<Schema>;
readonly allOf?: ReadonlyArray<Schema>;
};
}
export interface Schema extends OfSchema {
readonly type?: TypeStringef;
@@ -217,7 +217,7 @@ type ObjectSchemaTypeDef<p extends Schema> =
:
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any;
any
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;

View File

@@ -658,10 +658,4 @@ export class MiMeta {
default: '{}',
})
public federationHosts: string[];
@Column('varchar', {
length: 64,
nullable: true,
})
public googleAnalyticsMeasurementId: string | null;
}

View File

@@ -90,10 +90,6 @@ export type MiNotification = {
type: 'login';
id: string;
createdAt: string;
} | {
type: 'createToken';
id: string;
createdAt: string;
} | {
type: 'app';
id: string;

View File

@@ -288,24 +288,24 @@ export class MiUser {
export type MiLocalUser = MiUser & {
host: null;
uri: null;
};
}
export type MiPartialLocalUser = Partial<MiUser> & {
id: MiUser['id'];
host: null;
uri: null;
};
}
export type MiRemoteUser = MiUser & {
host: string;
uri: string;
};
}
export type MiPartialRemoteUser = Partial<MiUser> & {
id: MiUser['id'];
host: string;
uri: string;
};
}
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
export const passwordSchema = { type: 'string', minLength: 1 } as const;

View File

@@ -119,10 +119,6 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
googleAnalyticsMeasurementId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,

View File

@@ -332,16 +332,6 @@ export const packedNotificationSchema = {
enum: ['login'],
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['createToken'],
},
},
}, {
type: 'object',
properties: {

View File

@@ -92,7 +92,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
export type LoggerProps = {
disableQueryTruncation?: boolean;
enableQueryParamLogging?: boolean;
};
}
function highlightSql(sql: string) {
return highlight.highlight(sql, {

View File

@@ -29,7 +29,7 @@ export type ModeratorInactivityEvaluationResult = {
isModeratorsInactive: boolean;
inactiveModerators: MiUser[];
remainingTime: ModeratorInactivityRemainingTime;
};
}
export type ModeratorInactivityRemainingTime = {
time: number;

View File

@@ -107,12 +107,12 @@ export class InboxProcessorService implements OnApplicationShutdown {
// それでもわからなければ終了
if (authUser == null) {
throw new Bull.UnrecoverableError(`skip: failed to resolve user ${getApId(activity.actor)}`);
throw new Bull.UnrecoverableError('skip: failed to resolve user');
}
// publicKey がなくても終了
if (authUser.key == null) {
throw new Bull.UnrecoverableError(`skip: failed to resolve user publicKey ${getApId(activity.actor)}`);
throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
}
// HTTP-Signatureの検証

View File

@@ -38,7 +38,7 @@ export type RelationshipJobData = {
silent?: boolean;
requestId?: string;
withReplies?: boolean;
};
}
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
@@ -61,11 +61,11 @@ export type DbJobMap = {
importUserLists: DbUserImportJobData;
importCustomEmojis: DbUserImportJobData;
deleteAccount: DbUserDeleteJobData;
};
}
export type DbJobDataWithUser = {
user: ThinUser;
};
}
export type DbExportFollowingData = {
user: ThinUser;
@@ -75,7 +75,7 @@ export type DbExportFollowingData = {
export type DBExportAntennasData = {
user: ThinUser
};
}
export type DbUserDeleteJobData = {
user: ThinUser;
@@ -91,7 +91,7 @@ export type DbUserImportJobData = {
export type DBAntennaImportJobData = {
user: ThinUser,
antenna: Antenna
};
}
export type DbUserImportToDbJobData = {
user: ThinUser;

View File

@@ -103,43 +103,6 @@ export class ServerService implements OnApplicationShutdown {
serve: false,
});
// if the requester looks like to be performing an ActivityPub object lookup, reject all external redirects
//
// this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com
//
// this is not required by standard but protect us from peers that did not validate final URL.
if (this.config.disallowExternalApRedirect) {
const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i;
fastify.addHook('onSend', (request, reply, _, done) => {
const location = reply.getHeader('location');
if (reply.statusCode < 300 || reply.statusCode >= 400 || typeof location !== 'string') {
done();
return;
}
if (!maybeApLookupRegex.test(request.headers.accept ?? '')) {
done();
return;
}
const effectiveLocation = process.env.NODE_ENV === 'production' ? location : location.replace(/^http:\/\//, 'https://');
if (effectiveLocation.startsWith(`https://${this.config.host}/`)) {
done();
return;
}
reply.status(406);
reply.removeHeader('location');
reply.header('content-type', 'text/plain; charset=utf-8');
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
done(null, [
"Refusing to relay remote ActivityPub object lookup.",
"",
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
].join('\n'));
});
}
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.openApiServerService.createServer);
fastify.register(this.fileServerService.createServer);

View File

@@ -122,7 +122,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir
}) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & {
requireAdmin: true,
kind: (typeof permissions)[number],
});
})
export interface IEndpoint {
name: string;

View File

@@ -73,10 +73,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
googleAnalyticsMeasurementId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -516,7 +512,6 @@ export const meta = {
},
federation: {
type: 'string',
enum: ['all', 'specified', 'none'],
optional: false, nullable: false,
},
federationHosts: {
@@ -576,7 +571,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha,
googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,

View File

@@ -84,7 +84,6 @@ export const paramDef = {
turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true },
enableTestcaptcha: { type: 'boolean' },
googleAnalyticsMeasurementId: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -372,12 +371,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableTestcaptcha = ps.enableTestcaptcha;
}
if (ps.googleAnalyticsMeasurementId !== undefined) {
// 空文字列をnullにしたいので??は使わない
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
set.googleAnalyticsMeasurementId = ps.googleAnalyticsMeasurementId || null;
}
if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
}

View File

@@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
export const meta = {
tags: ['federation'],
@@ -54,6 +53,11 @@ export const meta = {
code: 'RESPONSE_INVALID',
id: '70193c39-54f3-4813-82f0-70a680f7495b',
},
responseInvalidIdHostNotMatch: {
message: 'Requested URI and response URI host does not match.',
code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
},
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@@ -149,8 +153,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
const object = await resolver.resolve(uri).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
// resolve
@@ -162,7 +165,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
throw new ApiError(meta.errors.federationNotAllowed);
case '72180409-793c-4973-868e-5a118eb5519b':
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
throw new ApiError(meta.errors.responseInvalid);
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
// resolveLocal
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':

View File

@@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userFollowingService.unfollow(follower, followee);
return await this.userEntityService.pack(follower.id, me);
return await this.userEntityService.pack(followee.id, me);
});
}
}

View File

@@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AccessTokensRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DI } from '@/di-symbols.js';
@@ -51,7 +50,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accessTokensRepository: AccessTokensRepository,
private idService: IdService,
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
// Generate access token
@@ -73,9 +71,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission: ps.permission,
});
// アクセストークンが生成されたことを通知
this.notificationService.createNotification(me.id, 'createToken', {});
return {
token: accessToken,
};

View File

@@ -210,15 +210,9 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? {
get: {
...info,
operationId: 'get___' + info.operationId,
},
get: info,
} : {}),
post: {
...info,
operationId: 'post___' + info.operationId,
},
post: info,
};
}

View File

@@ -82,8 +82,8 @@ export default abstract class Channel {
this.connection = connection;
}
public send(payload: { type: string, body: JsonValue }): void;
public send(type: string, payload: JsonValue): void;
public send(payload: { type: string, body: JsonValue }): void
public send(type: string, payload: JsonValue): void
@bindThis
public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
@@ -108,4 +108,4 @@ export type MiChannelService<T extends boolean> = {
requireCredential: T;
kind: T extends true ? string : string | null | undefined;
create: (id: string, connection: Connection) => Channel;
};
}

View File

@@ -114,17 +114,13 @@
if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
const reload = locale?.reload || 'Reload';
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<div class="message">${title}</div>
<div class="message">読み込みに失敗しました</div>
<div class="submessage">Failed to initialize Misskey</div>
<div class="submessage">Error Code: ${code}</div>
<button onclick="location.reload(!0)">
<div>${reload}</div>
<div>リロード</div>
<div><small>Reload</small></div>
</button>`;
addStyle(`
#misskey_app,

View File

@@ -151,22 +151,6 @@
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
const messages = Object.assign({
title: 'Failed to initialize Misskey',
solution: 'The following actions may solve the problem.',
solution1: 'Update your os and browser',
solution2: 'Disable an adblocker',
solution3: 'Clear the browser cache',
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
otherOption: 'Other options',
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
}, locale?._bootErrors || {});
const reload = locale?.reload || 'Reload';
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
@@ -176,32 +160,32 @@
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
</svg>
<h1>${messages.title}</h1>
<h1>Failed to load<br>読み込みに失敗しました</h1>
<button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">${reload}</span>
<span class="button-label-big">Reload / リロード</span>
</button>
<p><b>${messages.solution}</b></p>
<p>${messages.solution1}</p>
<p>${messages.solution2}</p>
<p>${messages.solution3}</p>
<p>${messages.solution4}</p>
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
<p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;">
<summary>${messages.otherOption}</summary>
<summary>Other options / その他のオプション</summary>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">${messages.otherOption1}</span>
<span class="button-label-small">Clear preferences and cache</span>
</button>
</a>
<br>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">${messages.otherOption2}</span>
<span class="button-label-small">Start the simple client</span>
</button>
</a>
<br>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">${messages.otherOption3}</span>
<span class="button-label-small">Start the repair tool</span>
</button>
</a>
</details>

View File

@@ -52,7 +52,7 @@ button {
.button-label-big {
color: #222;
font-weight: bold;
font-size: 1.2em;
font-size: 20px;
padding: 12px;
}
@@ -72,6 +72,11 @@ li {
font-size: 16px;
}
.dont-worry,
#msg {
font-size: 18px;
}
.icon-warning {
color: #dec340;
height: 4rem;
@@ -79,8 +84,7 @@ li {
}
h1 {
font-size: 1.5em;
margin: 1em;
font-size: 32px;
}
code {
@@ -96,16 +100,17 @@ code {
word-break: break-word;
}
#errorInfo summary {
summary {
cursor: pointer;
}
#errorInfo summary>* {
summary > * {
display: inline;
white-space: pre-wrap;
}
@media screen and (max-width: 500px) {
#errorInfo {
details {
width: 50%;
}
}

View File

@@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
'use strict';
(() => {
document.addEventListener('DOMContentLoaded', () => {
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
const messages = Object.assign({
title: 'Failed to initialize Misskey',
serverError: 'If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.',
solution: 'The following actions may solve the problem.',
solution1: 'Update your os and browser',
solution2: 'Disable an adblocker',
solution3: 'Clear the browser cache',
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
otherOption: 'Other options',
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
}, locale?._bootErrors || {});
const reload = locale?.reload || 'Reload';
const reloadEls = document.querySelectorAll('[data-i18n-reload]');
for (const el of reloadEls) {
el.textContent = reload;
}
const i18nEls = document.querySelectorAll('[data-i18n]');
for (const el of i18nEls) {
const key = el.dataset.i18n;
if (key && messages[key]) {
el.textContent = messages[key];
}
}
});
})();

View File

@@ -27,45 +27,39 @@ html
style
include ../error.css
script
include ../error.js
body
svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round")
path(stroke="none", d="M0 0h24v24H0z", fill="none")
path(d="M12 9v2m0 4v.01")
path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
h1(data-i18n="title") Failed to initialize Misskey
h1 An error has occurred!
button.button-big(onclick="location.reload();")
span.button-label-big(data-i18n-reload) Reload
span.button-label-big Refresh
p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
p.dont-worry Don't worry, it's (probably) not your fault.
p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
div#errors
code.
ERROR CODE: #{code}
ERROR ID: #{id}
p
b(data-i18n="solution") The following actions may solve the problem.
p You may also try the following options:
p(data-i18n="solution1") Update your os and browser
p(data-i18n="solution2") Disable an adblocker
p(data-i18n="solution3") Clear your browser cache
p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true
p Update your os and browser.
p Disable an adblocker.
details(style="color: #86b300;")
summary(data-i18n="otherOption") Other options
a(href="/flush")
button.button-small
span.button-label-small(data-i18n="otherOption1") Clear preferences and cache
span.button-label-small Clear preferences and cache
br
a(href="/cli")
button.button-small
span.button-label-small(data-i18n="otherOption2") Start the simple client
span.button-label-small Start the simple client
br
a(href="/bios")
button.button-small
span.button-label-small(data-i18n="otherOption3") Start the repair tool
span.button-label-small Start the repair tool

View File

@@ -18,7 +18,6 @@
* achievementEarned - 実績を獲得
* exportCompleted - エクスポートが完了
* login - ログイン
* createToken - トークン作成
* app - アプリ通知
* test - テスト通知(サーバー側)
*/
@@ -37,7 +36,6 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'createToken',
'app',
'test',
] as const;

View File

@@ -139,8 +139,7 @@ describe('Note', () => {
});
describe('Deletion', () => {
describe('Check Delete is delivered', () => {
describe('To followers', () => {
describe('Check Delete consistency', () => {
let carol: LoginUser;
beforeAll(async () => {
@@ -150,7 +149,7 @@ describe('Note', () => {
await sleep();
});
test('Check', async () => {
test('Delete is derivered to followers', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, carol);
await bob.client.request('notes/delete', { noteId: note.id });
@@ -164,75 +163,6 @@ describe('Note', () => {
},
);
});
afterAll(async () => {
await carol.client.request('following/delete', { userId: bobInA.id });
await sleep();
});
});
describe('To renoted and not followed user', () => {
test('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await alice.client.request('notes/create', { renoteId: noteInA.id });
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
describe('To replied and not followed user', () => {
test('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await alice.client.request('notes/create', { text: 'Hello Bob!', replyId: noteInA.id });
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
/**
* FIXME: not delivered
* @see https://github.com/misskey-dev/misskey/issues/15548
*/
describe('To only resolved and not followed user', () => {
test.failing('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await sleep();
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await alice.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
});
describe('Deletion of remote user\'s note for moderation', () => {

View File

@@ -22,7 +22,7 @@ export type LoginUser = SigninResponse & {
client: Misskey.api.APIClient;
username: string;
password: string;
};
}
/** used for avoiding overload and some endpoints */
export type Request = <

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://swc.rs/schema.json",
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",

View File

@@ -10,13 +10,13 @@ import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet
import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js';
// Request Accept in lowercase
// Request Accept
const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*';
// Response Content-Type in lowercase
// Response Content-Type
const AP = 'application/activity+json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';
const JSON_UTF8 = 'application/json; charset=utf-8';
@@ -44,8 +44,7 @@ describe('Webリソース', () => {
const { path, accept, cookie, type } = param;
const res = await simpleGet(path, accept, cookie);
assert.strictEqual(res.status, 200);
// Header values are case-insensitive
assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase());
assert.strictEqual(res.type, type ?? HTML);
return res;
};
@@ -96,7 +95,8 @@ describe('Webリソース', () => {
describe.each([
{ path: '/', type: HTML },
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
{ path: '/api-doc', type: HTML },
// fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
{ path: '/api-doc', type: 'text/html; charset=UTF-8' },
{ path: '/api.json', type: JSON_UTF8 },
{ path: '/api-console', type: HTML },
{ path: '/_info_card_', type: HTML },

View File

@@ -397,7 +397,7 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
}, 1000 * 30);
}, 1000 * 15);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);

View File

@@ -24,13 +24,13 @@ describe('MfmService', () => {
describe('toHtml', () => {
test('br', () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('br alt', () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});

View File

@@ -8,8 +8,6 @@ import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { IObject } from '@/core/activitypub/type.js';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return {
@@ -26,10 +24,6 @@ export const buildParsedSignature = (signingString: string, signature: string, a
};
};
function cartesianProduct<T, U>(a: T[], b: U[]): [T, U][] {
return a.flatMap(a => b.map(b => [a, b] as [T, U]));
}
describe('ap-request', () => {
test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
@@ -64,123 +58,4 @@ describe('ap-request', () => {
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
test('rejects non matching domain', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://alice.example.com/abc' } as IObject,
[
'https://alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict,
), 'validation should pass base case');
assert.throws(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject,
[
'https://alice.example.com/abc',
],
FetchAllowSoftFailMask.Any,
), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
// fix issues like threads
// https://github.com/misskey-dev/misskey/issues/15039
const withOrWithoutWWW = [
'https://alice.example.com/abc',
'https://www.alice.example.com/abc',
];
cartesianProduct(
cartesianProduct(
withOrWithoutWWW,
withOrWithoutWWW,
),
withOrWithoutWWW,
).forEach(([[a, b], c]) => {
assert.doesNotThrow(() => assertActivityMatchesUrls(
a,
{ id: b } as IObject,
[
c,
],
FetchAllowSoftFailMask.Strict,
), 'validation should pass with or without www. subdomain');
});
});
test('cross origin lookup', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject,
[
'https://bob.example.com/abc',
],
FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
assert.throws(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://bob.example.com/abc' } as IObject,
[
'https://bob.example.com/abc',
],
FetchAllowSoftFailMask.Strict,
), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
});
test('rejects non-canonical ID', () => {
assert.throws(() => assertActivityMatchesUrls(
'https://alice.example.com/@alice',
{ id: 'https://alice.example.com/users/alice' } as IObject,
[
'https://alice.example.com/users/alice'
],
FetchAllowSoftFailMask.Strict,
), 'throws if the response ID did not exactly match the expected ID');
assert.doesNotThrow(() => assertActivityMatchesUrls(
'https://alice.example.com/@alice',
{ id: 'https://alice.example.com/users/alice' } as IObject,
[
'https://alice.example.com/users/alice',
],
FetchAllowSoftFailMask.NonCanonicalId,
), 'does not throw if non-canonical ID is allowed');
});
test('origin relaxed alignment', () => {
assert.doesNotThrow(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://ap.alice.example.com/abc' } as IObject,
[
'https://ap.alice.example.com/abc',
],
FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should pass if response is a subdomain of the expected origin');
assert.throws(() => assertActivityMatchesUrls(
'https://alice.multi-tenant.example.com/abc',
{ id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
[
'https://bob.multi-tenant.example.com/abc',
],
FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
), 'validation should fail if response is a disjoint domain of the expected origin');
assert.throws(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://ap.alice.example.com/abc' } as IObject,
[
'https://ap.alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict,
), 'throws if relaxed origin is forbidden');
});
test('resist HTTP downgrade', () => {
assert.throws(() => assertActivityMatchesUrls(
'https://alice.example.com/abc',
{ id: 'https://alice.example.com/abc' } as IObject,
[
'http://alice.example.com/abc',
],
FetchAllowSoftFailMask.Strict,
), 'throws if HTTP downgrade is detected');
});
});

View File

@@ -12,12 +12,12 @@
"dependencies": {
"@discordapp/twemoji": "15.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.3",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -25,46 +25,47 @@
"misskey-js": "workspace:*",
"frontend-shared": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.34.8",
"sass": "1.85.0",
"shiki": "3.0.0",
"rollup": "4.26.0",
"sass": "1.79.4",
"shiki": "1.22.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.7.3",
"uuid": "11.1.0",
"typescript": "5.6.3",
"uuid": "10.0.0",
"json5": "2.2.3",
"vite": "6.1.1",
"vue": "3.5.13"
"vite": "5.4.11",
"vue": "3.5.12"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.0",
"@misskey-dev/summaly": "5.1.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.6",
"@types/micromatch": "4.0.9",
"@types/node": "22.13.5",
"@types/node": "22.9.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14",
"@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.24.1",
"@vitest/coverage-v8": "3.0.6",
"@vue/runtime-core": "3.5.13",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0",
"fast-glob": "3.3.3",
"happy-dom": "17.1.4",
"eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.7.1",
"nodemon": "3.1.9",
"prettier": "3.5.2",
"start-server-and-test": "2.0.10",
"msw": "2.6.4",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"start-server-and-test": "2.0.8",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.2.4",
"vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4"
"vue-tsc": "2.1.10"
}
}

View File

@@ -21,7 +21,7 @@ export type MiPostMessageEvent<T extends PostMessageEventType = PostMessageEvent
type: T;
iframeId?: string;
payload?: PostMessageEventPayload[T];
};
}
let defaultIframeId: string | null = null;

View File

@@ -98,12 +98,4 @@ export default [
'vue/attribute-hyphenation': ['error', 'never'],
},
},
{
ignores: [
// TODO: Error while loading rule '@typescript-eslint/naming-convention': Cannot use 'in' operator to search for 'type' in undefined のため一時的に無効化
// See https://github.com/misskey-dev/misskey/pull/15311
'js/i18n.ts',
'js-built/',
],
},
];

View File

@@ -69,7 +69,6 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'createToken',
'test',
'app',
] as const;

View File

@@ -6,7 +6,7 @@
//#region Embed関連の定義
/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */
export const embeddableEntities = [
const embeddableEntities = [
'notes',
'user-timeline',
'clips',

View File

@@ -9,10 +9,10 @@ export type UnicodeEmojiDef = {
name: string;
char: string;
category: typeof unicodeEmojiCategories[number];
};
}
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from './emojilist.json' with { type: 'json' };
import _emojilist from './emojilist.json';
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,

View File

@@ -2,7 +2,6 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ILocale, ParameterizedString } from '../../../locales/index.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -134,6 +134,7 @@ export function scrollToBottom(
export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
const scrollTop = getScrollPosition(el);
if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance);
return scrollTop <= tolerance;
}

View File

@@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.13.5",
"@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.24.1",
"esbuild": "0.25.0",
"eslint-plugin-vue": "9.32.0",
"nodemon": "3.1.9",
"typescript": "5.7.3",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"esbuild": "0.24.0",
"eslint-plugin-vue": "9.31.0",
"nodemon": "3.1.7",
"typescript": "5.6.3",
"vue-eslint-parser": "9.4.3"
},
"files": [
@@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.13"
"vue": "3.5.12"
}
}

View File

@@ -414,7 +414,6 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'),
glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkImgPreviewDialog.vue'),
glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'),
glob('src/components/MkTagItem.vue'),

View File

@@ -47,8 +47,6 @@ export default [
'@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true,
}],
// defineExposeが誤検知されてしまう
'@typescript-eslint/no-unused-expressions': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため

View File

@@ -16,34 +16,32 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "15.1.0",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.3",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.4.8",
"chart.js": "4.4.6",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.25.2",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.18.1",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2",
"date-fns": "4.1.0",
"date-fns": "2.30.0",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
"frontend-shared": "workspace:*",
@@ -51,91 +49,92 @@
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
"matter-js": "0.19.0",
"mfm-js": "0.24.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.34.8",
"sanitize-html": "2.14.0",
"sass": "1.85.0",
"shiki": "3.0.0",
"rollup": "4.26.0",
"sanitize-html": "2.13.1",
"sass": "1.79.3",
"shiki": "1.22.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.173.0",
"three": "0.169.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.7.3",
"uuid": "11.1.0",
"typescript": "5.6.3",
"uuid": "10.0.0",
"v-code-diff": "1.13.1",
"vite": "6.1.1",
"vue": "3.5.13",
"vite": "5.4.11",
"vue": "3.5.12",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.0",
"@storybook/addon-actions": "8.5.8",
"@storybook/addon-essentials": "8.5.8",
"@storybook/addon-interactions": "8.5.8",
"@storybook/addon-links": "8.5.8",
"@storybook/addon-mdx-gfm": "8.5.8",
"@storybook/addon-storysource": "8.5.8",
"@storybook/blocks": "8.5.8",
"@storybook/components": "8.5.8",
"@storybook/core-events": "8.5.8",
"@storybook/manager-api": "8.5.8",
"@storybook/preview-api": "8.5.8",
"@storybook/react": "8.5.8",
"@storybook/react-vite": "8.5.8",
"@storybook/test": "8.5.8",
"@storybook/theming": "8.5.8",
"@storybook/types": "8.5.8",
"@storybook/vue3": "8.5.8",
"@storybook/vue3-vite": "8.5.8",
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.4.4",
"@storybook/addon-essentials": "8.4.4",
"@storybook/addon-interactions": "8.4.4",
"@storybook/addon-links": "8.4.4",
"@storybook/addon-mdx-gfm": "8.4.4",
"@storybook/addon-storysource": "8.4.4",
"@storybook/blocks": "8.4.4",
"@storybook/components": "8.4.4",
"@storybook/core-events": "8.4.4",
"@storybook/manager-api": "8.4.4",
"@storybook/preview-api": "8.4.4",
"@storybook/react": "8.4.4",
"@storybook/react-vite": "8.4.4",
"@storybook/test": "8.4.4",
"@storybook/theming": "8.4.4",
"@storybook/types": "8.4.4",
"@storybook/vue3": "8.4.4",
"@storybook/vue3-vite": "8.4.4",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.8",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "22.13.5",
"@types/node": "22.9.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14",
"@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.24.1",
"@vitest/coverage-v8": "3.0.6",
"@vue/runtime-core": "3.5.13",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"cypress": "14.0.3",
"cypress": "13.15.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0",
"fast-glob": "3.3.3",
"happy-dom": "17.1.4",
"eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.7.1",
"msw": "2.6.4",
"msw-storybook-addon": "2.0.4",
"nodemon": "3.1.9",
"prettier": "3.5.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.10",
"storybook": "8.5.8",
"start-server-and-test": "2.0.8",
"storybook": "8.4.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.0.6",
"vitest-fetch-mock": "0.4.3",
"vue-component-type-helpers": "2.2.4",
"vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4"
"vue-tsc": "2.1.10"
}
}

View File

@@ -1,107 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics';
/**
* analytics moduleを読み込まなくても動作するようにするためのラッパー
*/
class AnalyticsProxy implements AnalyticsInstance {
private analytics?: AnalyticsInstance;
constructor(analytics?: AnalyticsInstance) {
if (analytics) {
this.analytics = analytics;
}
}
public setAnalytics(analytics: AnalyticsInstance) {
if (this.analytics) {
throw new Error('Analytics instance already exists.');
}
this.analytics = analytics;
}
public identify(...args: Parameters<AnalyticsInstance['identify']>) {
return this.analytics?.identify(...args) ?? Promise.resolve();
}
public track(...args: Parameters<AnalyticsInstance['track']>) {
return this.analytics?.track(...args) ?? Promise.resolve();
}
public page(...args: Parameters<AnalyticsInstance['page']>) {
return this.analytics?.page(...args) ?? Promise.resolve();
}
public user(...args: Parameters<AnalyticsInstance['user']>) {
return this.analytics?.user(...args) ?? Promise.resolve();
}
public reset(...args: Parameters<AnalyticsInstance['reset']>) {
return this.analytics?.reset(...args) ?? Promise.resolve();
}
public ready(...args: Parameters<AnalyticsInstance['ready']>) {
return this.analytics?.ready(...args) ?? function () { void 0; };
}
public on(...args: Parameters<AnalyticsInstance['on']>) {
return this.analytics?.on(...args) ?? function () { void 0; };
}
public once(...args: Parameters<AnalyticsInstance['once']>) {
return this.analytics?.once(...args) ?? function () { void 0; };
}
public getState(...args: Parameters<AnalyticsInstance['getState']>) {
return this.analytics?.getState(...args) ?? Promise.resolve();
}
public get storage() {
return this.analytics?.storage ?? {
getItem: () => null,
setItem: () => void 0,
removeItem: () => void 0,
};
}
public get plugins() {
return this.analytics?.plugins ?? {
enable: (p, c) => Promise.resolve(c ? c() : void 0),
disable: (p, c) => Promise.resolve(c ? c() : void 0),
};
}
}
export const analytics = new AnalyticsProxy();
export async function initAnalytics(instance: Misskey.entities.MetaDetailed) {
// アナリティクスプロバイダに関する設定がひとつもない場合は、アナリティクスモジュールを読み込まない
if (!instance.googleAnalyticsMeasurementId) {
return;
}
const { default: Analytics } = await import('analytics');
const plugins: AnalyticsPlugin[] = [];
// Google Analytics
if (instance.googleAnalyticsMeasurementId) {
const { default: googleAnalytics } = await import('@analytics/google-analytics');
plugins.push(googleAnalytics({
measurementIds: [instance.googleAnalyticsMeasurementId],
debug: _DEV_,
}));
}
analytics.setAnalytics(Analytics({
app: 'misskey',
version: _VERSION_,
debug: _DEV_,
plugins,
}));
}

View File

@@ -4,9 +4,9 @@
*/
import { computed, watch, version as vueVersion } from 'vue';
import type { App } from 'vue';
import { compareVersions } from 'compare-versions';
import { version, lang, updateLocale, locale } from '@@/js/config.js';
import type { App } from 'vue';
import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js';
import components from '@/components/index.js';
@@ -21,7 +21,6 @@ import { reloadChannel } from '@/scripts/unison-reload.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { analytics, initAnalytics } from '@/analytics.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/main.js';
@@ -242,19 +241,6 @@ export async function common(createVue: () => App<Element>) {
await fetchCustomEmojis();
} catch (err) { /* empty */ }
// analytics
fetchInstanceMetaPromise.then(async () => {
await initAnalytics(instance);
if ($i) {
analytics.identify($i.id);
}
analytics.page({
path: window.location.pathname,
});
});
const app = createVue();
setupRouter(app, createMainRouter);

Some files were not shown because too many files have changed in this diff Show More