Compare commits

...

27 Commits

Author SHA1 Message Date
zyoshoka
a492f2c7e1 chore(deps): manually update node.js to v22.14.0 2025-03-05 23:12:18 +09:00
renovate[bot]
c7cbe60a2d chore(deps): update node.js to v22.14.0 2025-03-05 14:01:55 +00:00
renovate[bot]
f3be426383 fix(deps): update [frontend] update dependencies (#15595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 21:01:44 +09:00
かっこかり
e8a6629cb5 fix(backend): システムアカウント系のマイグレーション不足を修正 (#15586)
* fix(backend): プロキシアカウントのロールバック用マイグレーションを追加

* fix

* separate newly-added `up` command

* drop backwards-compatibility

* docs
2025-03-05 16:49:49 +09:00
github-actions[bot]
44658ae981 Bump version to 2025.3.0-beta.0 2025-03-05 07:44:38 +00:00
syuilo
19384efbc5 clean up 2025-03-04 18:48:14 +09:00
かっこかり
adf22143aa Revert "enhance(frontend): チャンネル投稿をユーザーページと前後ノートに表示する" (#15589)
* Revert "enhance(frontend): チャンネル投稿をユーザーページと前後ノートに表示する (#15532)"

This reverts commit a4711ab4c1.

* Update CHANGELOG.md
2025-03-03 21:05:50 +09:00
renovate[bot]
a17acf647b fix(deps): update [frontend] update dependencies (#15587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 17:48:17 +09:00
かっこかり
01a3eabc4e enhance(frontend): アニメーション設定で画面上のエフェクトも考慮するように (#15576)
* enhance(frontend): アニメーション設定で画面上のエフェクトも考慮するように

* Update Changelog
2025-03-03 08:46:38 +00:00
かっこかり
59567a7ccc fix(frontend): 照会処理を統一 (#15536)
* fix(frontend): 照会処理を統一

* fix

* doLookup -> apLookup
2025-03-03 08:45:04 +00:00
かっこかり
7fb8fccd57 Update CHANGELOG.md for #15532 2025-03-03 17:29:49 +09:00
鴇峰 朔華
a4711ab4c1 enhance(frontend): チャンネル投稿をユーザーページと前後ノートに表示する (#15532)
* enhance(frontend): ユーザーページで常にチャンネル投稿が含まれるように

* enhance(frontend): ノート詳細の前後の投稿にチャンネル投稿を含めるように

* ログイン有無の削除
2025-03-03 08:28:29 +00:00
かっこかり
bbe404a0b2 fix(frontend): 投稿フォームがオーバーフローした際にスクロールできるように (#15571)
* fix(frontend): 投稿フォームがオーバーフローした際にスクロールできるように

* Update Changelog

* remove unused props
2025-03-03 08:17:20 +00:00
かっこかり
0610bd657f fix(frontend): フォローされたときのメッセージのshadowがちらつくことがある問題を修正 (#15584)
* fix(frontend): フォローされたときのメッセージがちらつく問題を修正

* Update Changelog
2025-03-03 08:09:41 +00:00
かっこかり
77667cf80d enhance(frontend): モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように (#15462)
* enhance(frontend): モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように

* use MkSwitch

* Update Changelog
2025-03-03 08:06:34 +00:00
tetsuya-ki
801a2ec1db fix(frontend): 削除して編集の削除タイミングを投稿後になるように #14498 (#15545)
* fix #14498

- 「削除して編集」の削除タイミングを投稿したタイミングへ変更

* update CHANGELOG.md

* 指摘対応

- InitialNoteがあれば必ず削除するべきものでもないため、投稿後にノートを削除するフラグをプロパティに追加

* 指摘対応のミス修正

- フラグを条件に追加
- 実績のdateが数値になってなかった点を修正

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-03-03 08:05:18 +00:00
github-actions[bot]
2a96e39bb3 Bump version to 2025.3.0-alpha.0 2025-03-02 12:12:06 +00:00
syuilo
616cccf251 enhance(backend): refine system account (#15530)
* wip

* wip

* wip

* Update SystemAccountService.ts

* Update 1740121393164-system-accounts.js

* Update DeleteAccountService.ts

* wip

* wip

* wip

* wip

* Update 1740121393164-system-accounts.js

* Update RepositoryModule.ts

* wip

* wip

* wip

* Update ApRendererService.ts

* wip

* wip

* Update SystemAccountService.ts

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* add print logs

* ログが長すぎて出てないかもしれない

* fix migration

* refactor

* fix fed-tests

* Update RelayService.ts

* merge

* Update user.test.ts

* chore: emit log

* fix: tweak sleep duration

* fix: exit 1

* fix: wait for misskey processes to become healthy

* fix: longer sleep for user deletion

* fix: make sleep longer again

* デッドロック解消の試み

https://github.com/misskey-dev/misskey/issues/15005

* Revert "デッドロック解消の試み"

This reverts commit 266141f66f.

* wip

* Update SystemAccountService.ts

---------

Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-03-02 20:06:20 +09:00
syuilo
7114523d84 Update CHANGELOG.md 2025-03-01 17:02:52 +09:00
syuilo
5d683728f3 デッドロック解消の試み (#15574)
https://github.com/misskey-dev/misskey/issues/15005

Co-authored-by: 饺子w (Yumechi) <35571479+eternal-flame-AD@users.noreply.github.com>
2025-03-01 16:12:42 +09:00
おさむのひと
b8632f389d chore(ci): Renovateが作ったprにラベルつける (#15573) 2025-03-01 04:37:11 +00:00
renovate[bot]
830da5e9f1 fix(deps): update [frontend] update dependencies (#15566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:11:09 +09:00
renovate[bot]
e2eddd5b1a chore(deps): update actions/cache action to v4.2.2 (#15564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:59 +09:00
renovate[bot]
d4f9bf1f11 chore(deps): update [misskey-js] update dependencies (#15565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:47 +09:00
renovate[bot]
734c78ddd1 chore(deps): update [tools] update dependencies (#15563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:36 +09:00
syuilo
c63c3462dd refactor 2025-02-28 09:34:21 +09:00
github-actions[bot]
a3bba23b7d [skip ci] Update CHANGELOG.md (prepend template) 2025-02-27 08:58:46 +00:00
119 changed files with 2425 additions and 1986 deletions

View File

@@ -5,7 +5,7 @@
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0" "version": "22.14.0"
}, },
"ghcr.io/devcontainers-extra/features/corepack:1": { "ghcr.io/devcontainers-extra/features/corepack:1": {
"version": "0.31.0" "version": "0.31.0"

View File

@@ -21,7 +21,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
api-json-name: [api-base.json, api-head.json] api-json-name: [api-base.json, api-head.json]
include: include:
- api-json-name: api-base.json - api-json-name: api-base.json

View File

@@ -79,7 +79,7 @@ jobs:
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Restore eslint cache - name: Restore eslint cache
uses: actions/cache@v4.2.1 uses: actions/cache@v4.2.2
with: with:
path: ${{ env.eslint-cache-path }} path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}

View File

@@ -20,7 +20,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2

View File

@@ -29,7 +29,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
services: services:
postgres: postgres:
@@ -92,7 +92,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
services: services:
postgres: postgres:

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -62,14 +62,30 @@ jobs:
bash ./setup.sh bash ./setup.sh
sudo chmod 644 ./certificates/*.test.key sudo chmod 644 ./certificates/*.test.key
- name: Start servers - name: Start servers
id: start_servers
continue-on-error: true
# https://github.com/docker/compose/issues/1294#issuecomment-374847206 # https://github.com/docker/compose/issues/1294#issuecomment-374847206
run: | run: |
cd packages/backend/test-federation cd packages/backend/test-federation
docker compose up -d --scale tester=0 docker compose up -d --scale tester=0
- name: Print start_servers error
if: ${{ steps.start_servers.outcome == 'failure' }}
run: |
cd packages/backend/test-federation
docker compose logs | tail -n 300
exit 1
- name: Test - name: Test
id: test
continue-on-error: true
run: | run: |
cd packages/backend/test-federation cd packages/backend/test-federation
docker compose run --no-deps tester docker compose run --no-deps tester
- name: Log
if: ${{ steps.test.outcome == 'failure' }}
run: |
cd packages/backend/test-federation
docker compose logs
exit 1
- name: Stop servers - name: Stop servers
run: | run: |
cd packages/backend/test-federation cd packages/backend/test-federation

View File

@@ -33,7 +33,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2
@@ -69,7 +69,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
browser: [chrome] browser: [chrome]
services: services:

View File

@@ -26,7 +26,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

View File

@@ -18,7 +18,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2

View File

@@ -22,7 +22,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.11.0] node-version: [22.14.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2

View File

@@ -1 +1 @@
22.11.0 22.14.0

View File

@@ -1,3 +1,20 @@
## 2025.3.0
### General
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
- Fix: システムアカウントが削除できる問題を修正
### Client
- Enhance: モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように
- Enhance: 「UIのアニメーションを減らす」で画面上のエフェクトも減らせるように
- Fix: 削除して編集の削除タイミングを投稿後になるように `#14498`
- Fix: フォローされたときのメッセージがちらつくことがある問題を修正
- Fix: 投稿ダイアログがサイズ限界を超えた際にスクロールできない問題を修正
### Server
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
## 2025.2.1 ## 2025.2.1
### General ### General

View File

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

View File

@@ -233,7 +233,7 @@ describe('After user setup', () => {
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click(); cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!'); cy.contains('Hello, Misskey!', { timeout: 15000 });
}); });
it('open note form with hotkey', () => { it('open note form with hotkey', () => {

12
locales/index.d.ts vendored
View File

@@ -5262,6 +5262,14 @@ export interface Locale extends ILocale {
* " {emoji} " をリアクションしますか? * " {emoji} " をリアクションしますか?
*/ */
"reactAreYouSure": ParameterizedString<"emoji">; "reactAreYouSure": ParameterizedString<"emoji">;
/**
* このメディアをセンシティブとして設定しますか?
*/
"markAsSensitiveConfirm": string;
/**
* このメディアのセンシティブ指定を解除しますか?
*/
"unmarkAsSensitiveConfirm": string;
"_accountSettings": { "_accountSettings": {
/** /**
* コンテンツの表示にログインを必須にする * コンテンツの表示にログインを必須にする
@@ -10058,6 +10066,10 @@ export interface Locale extends ILocale {
* ギャラリーの投稿を削除 * ギャラリーの投稿を削除
*/ */
"deleteGalleryPost": string; "deleteGalleryPost": string;
/**
* プロキシアカウントの説明を更新
*/
"updateProxyAccountDescription": string;
}; };
"_fileViewer": { "_fileViewer": {
/** /**

View File

@@ -1311,6 +1311,8 @@ federationSpecified: "このサーバーはホワイトリスト連合で運用
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
confirmOnReact: "リアクションする際に確認する" confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?" reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
_accountSettings: _accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする" requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@@ -2664,6 +2666,7 @@ _moderationLogTypes:
deletePage: "ページを削除" deletePage: "ページを削除"
deleteFlash: "Playを削除" deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除" deleteGalleryPost: "ギャラリーの投稿を削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer: _fileViewer:
title: "ファイルの詳細" title: "ファイルの詳細"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.2.1", "version": "2025.3.0-beta.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -25,7 +25,7 @@
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate", "init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate", "migrate": "cd packages/backend && pnpm migrate",
"revert": "cd packages/backend && pnpm revert", "revert": "cd packages/backend && pnpm revert",
@@ -37,7 +37,7 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run", "cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", "e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm jest", "jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm -r test", "test": "pnpm -r test",

View File

@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts1740121393164 {
name = 'SystemAccounts1740121393164'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "system_account" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(256) NOT NULL, CONSTRAINT "PK_edb56f4aaf9ddd50ee556da97ba" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_41a3c87a37aea616ee459369e1" ON "system_account" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c362033aee0ea51011386a5a7e" ON "system_account" ("type") `);
await queryRunner.query(`ALTER TABLE "system_account" ADD CONSTRAINT "FK_41a3c87a37aea616ee459369e12" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor'`);
if (instanceActor.length > 0) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${instanceActor[0].id}', '${instanceActor[0].id}', 'actor')`);
}
const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor'`);
if (relayActor.length > 0) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${relayActor[0].id}', '${relayActor[0].id}', 'relay')`);
}
const meta = await queryRunner.query(`SELECT "proxyAccountId" FROM "meta" ORDER BY "id" DESC LIMIT 1`);
if (!meta && meta.length >= 1 && meta[0].proxyAccountId) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${meta[0].proxyAccountId}', '${meta[0].proxyAccountId}', 'proxy')`);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "system_account" DROP CONSTRAINT "FK_41a3c87a37aea616ee459369e12"`);
await queryRunner.query(`DROP INDEX "public"."IDX_c362033aee0ea51011386a5a7e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_41a3c87a37aea616ee459369e1"`);
await queryRunner.query(`DROP TABLE "system_account"`);
}
}

View File

@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts21740129169650 {
name = 'SystemAccounts21740129169650'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`);
const proxyAccountId = await queryRunner.query(`SELECT "userId" FROM "system_account" WHERE "type" = 'proxy' ORDER BY "id" DESC LIMIT 1`);
if (proxyAccountId && proxyAccountId.length >= 1) {
await queryRunner.query(`UPDATE "meta" SET "proxyAccountId" = '${proxyAccountId[0].userId}'`);
}
await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}
}

View File

@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts31740133121105 {
name = 'SystemAccounts31740133121105'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "rootUserId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc" FOREIGN KEY ("rootUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
const users = await queryRunner.query(`SELECT "id" FROM "user" WHERE "isRoot" = true LIMIT 1`);
if (users.length > 0) {
await queryRunner.query(`UPDATE "meta" SET "rootUserId" = $1`, [users[0].id]);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "rootUserId"`);
}
}

View File

@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts41740993126937 {
name = 'SystemAccounts41740993126937'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isRoot"`);
}
async down(queryRunner) {
// down 実行時は isRoot = true のユーザーが存在しなくなるため手動で対応する必要あり
await queryRunner.query(`ALTER TABLE "user" ADD "isRoot" boolean NOT NULL DEFAULT false`);
}
}

View File

@@ -133,7 +133,7 @@ const $meta: Provider = {
for (const key in body.after) { for (const key in body.after) {
(meta as any)[key] = (body.after as any)[key]; (meta as any)[key] = (body.after as any)[key];
} }
meta.proxyAccount = null; // joinなカラムは通常取ってこないので meta.rootUser = null; // joinなカラムは通常取ってこないので
break; break;
} }
default: default:

View File

@@ -10,9 +10,9 @@ import { bindThis } from '@/decorators.js';
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js'; import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdService } from './IdService.js'; import { IdService } from './IdService.js';
@Injectable() @Injectable()
@@ -27,7 +27,7 @@ export class AbuseReportService {
private idService: IdService, private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService, private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService, private queueService: QueueService,
private instanceActorService: InstanceActorService, private systemAccountService: SystemAccountService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
) { ) {
@@ -136,7 +136,7 @@ export class AbuseReportService {
forwarded: true, forwarded: true,
}); });
const actor = await this.instanceActorService.getInstanceActor(); const actor = await this.systemAccountService.fetch('actor');
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);

View File

@@ -20,10 +20,10 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable() @Injectable()
export class AccountMoveService { export class AccountMoveService {
@@ -55,12 +55,12 @@ export class AccountMoveService {
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
private relayService: RelayService, private relayService: RelayService,
private queueService: QueueService, private queueService: QueueService,
private systemAccountService: SystemAccountService,
) { ) {
} }
@@ -126,11 +126,11 @@ export class AccountMoveService {
} }
// follow the new account // follow the new account
const proxy = await this.proxyAccountService.fetch(); const proxy = await this.systemAccountService.fetch('proxy');
const followings = await this.followingsRepository.findBy({ const followings = await this.followingsRepository.findBy({
followeeId: src.id, followeeId: src.id,
followerHost: IsNull(), // follower is local followerHost: IsNull(), // follower is local
followerId: proxy ? Not(proxy.id) : undefined, followerId: Not(proxy.id),
}); });
const followJobs = followings.map(following => ({ const followJobs = followings.map(following => ({
from: { id: following.followerId }, from: { id: following.followerId },
@@ -250,10 +250,8 @@ export class AccountMoveService {
// Have the proxy account follow the new account in the same way as UserListService.push // Have the proxy account follow the new account in the same way as UserListService.push
if (this.userEntityService.isRemoteUser(dst)) { if (this.userEntityService.isRemoteUser(dst)) {
const proxy = await this.proxyAccountService.fetch(); const proxy = await this.systemAccountService.fetch('proxy');
if (proxy) { this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
}
} }
} }

View File

@@ -24,7 +24,6 @@ import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js'; import { AchievementService } from './AchievementService.js';
import { AvatarDecorationService } from './AvatarDecorationService.js'; import { AvatarDecorationService } from './AvatarDecorationService.js';
import { CaptchaService } from './CaptchaService.js'; import { CaptchaService } from './CaptchaService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js'; import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js'; import { DeleteAccountService } from './DeleteAccountService.js';
import { DownloadService } from './DownloadService.js'; import { DownloadService } from './DownloadService.js';
@@ -37,7 +36,7 @@ import { HashtagService } from './HashtagService.js';
import { HttpRequestService } from './HttpRequestService.js'; import { HttpRequestService } from './HttpRequestService.js';
import { IdService } from './IdService.js'; import { IdService } from './IdService.js';
import { ImageProcessingService } from './ImageProcessingService.js'; import { ImageProcessingService } from './ImageProcessingService.js';
import { InstanceActorService } from './InstanceActorService.js'; import { SystemAccountService } from './SystemAccountService.js';
import { InternalStorageService } from './InternalStorageService.js'; import { InternalStorageService } from './InternalStorageService.js';
import { MetaService } from './MetaService.js'; import { MetaService } from './MetaService.js';
import { MfmService } from './MfmService.js'; import { MfmService } from './MfmService.js';
@@ -69,7 +68,6 @@ import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js'; import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js'; import { VideoProcessingService } from './VideoProcessingService.js';
import { UserWebhookService } from './UserWebhookService.js'; import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js'; import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js'; import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js'; import { SearchService } from './SearchService.js';
@@ -167,7 +165,6 @@ const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppL
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService }; const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService };
@@ -180,7 +177,6 @@ const $HashtagService: Provider = { provide: 'HashtagService', useExisting: Hash
const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService }; const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
const $IdService: Provider = { provide: 'IdService', useExisting: IdService }; const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService }; const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService }; const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService }; const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
@@ -191,7 +187,7 @@ const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExisting: ProxyAccountService }; const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService }; const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
@@ -318,7 +314,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AchievementService, AchievementService,
AvatarDecorationService, AvatarDecorationService,
CaptchaService, CaptchaService,
CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
DownloadService, DownloadService,
@@ -331,7 +326,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HttpRequestService, HttpRequestService,
IdService, IdService,
ImageProcessingService, ImageProcessingService,
InstanceActorService,
InternalStorageService, InternalStorageService,
MetaService, MetaService,
MfmService, MfmService,
@@ -342,7 +336,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteReadService, NoteReadService,
NotificationService, NotificationService,
PollService, PollService,
ProxyAccountService, SystemAccountService,
PushNotificationService, PushNotificationService,
QueryService, QueryService,
ReactionService, ReactionService,
@@ -465,7 +459,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AchievementService, $AchievementService,
$AvatarDecorationService, $AvatarDecorationService,
$CaptchaService, $CaptchaService,
$CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
$DownloadService, $DownloadService,
@@ -478,7 +471,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$HttpRequestService, $HttpRequestService,
$IdService, $IdService,
$ImageProcessingService, $ImageProcessingService,
$InstanceActorService,
$InternalStorageService, $InternalStorageService,
$MetaService, $MetaService,
$MfmService, $MfmService,
@@ -489,7 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteReadService, $NoteReadService,
$NotificationService, $NotificationService,
$PollService, $PollService,
$ProxyAccountService, $SystemAccountService,
$PushNotificationService, $PushNotificationService,
$QueryService, $QueryService,
$ReactionService, $ReactionService,
@@ -613,7 +605,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AchievementService, AchievementService,
AvatarDecorationService, AvatarDecorationService,
CaptchaService, CaptchaService,
CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
DownloadService, DownloadService,
@@ -626,7 +617,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HttpRequestService, HttpRequestService,
IdService, IdService,
ImageProcessingService, ImageProcessingService,
InstanceActorService,
InternalStorageService, InternalStorageService,
MetaService, MetaService,
MfmService, MfmService,
@@ -637,7 +627,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteReadService, NoteReadService,
NotificationService, NotificationService,
PollService, PollService,
ProxyAccountService, SystemAccountService,
PushNotificationService, PushNotificationService,
QueryService, QueryService,
ReactionService, ReactionService,
@@ -759,7 +749,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AchievementService, $AchievementService,
$AvatarDecorationService, $AvatarDecorationService,
$CaptchaService, $CaptchaService,
$CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
$DownloadService, $DownloadService,
@@ -772,7 +761,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$HttpRequestService, $HttpRequestService,
$IdService, $IdService,
$ImageProcessingService, $ImageProcessingService,
$InstanceActorService,
$InternalStorageService, $InternalStorageService,
$MetaService, $MetaService,
$MfmService, $MfmService,
@@ -783,7 +771,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteReadService, $NoteReadService,
$NotificationService, $NotificationService,
$PollService, $PollService,
$ProxyAccountService, $SystemAccountService,
$PushNotificationService, $PushNotificationService,
$QueryService, $QueryService,
$ReactionService, $ReactionService,

View File

@@ -1,86 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { IsNull, DataSource } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import { DI } from '@/di-symbols.js';
import generateNativeUserToken from '@/misc/generate-native-user-token.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class CreateSystemUserService {
constructor(
@Inject(DI.db)
private db: DataSource,
private idService: IdService,
) {
}
@bindThis
public async createSystemUser(username: string): Promise<MiUser> {
const password = randomUUID();
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair();
let account!: MiUser;
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: username.toLowerCase(),
host: IsNull(),
});
if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(MiUser, {
id: this.idService.gen(),
username: username,
usernameLower: username.toLowerCase(),
host: null,
token: secret,
isRoot: false,
isLocked: true,
isExplorable: false,
isBot: true,
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id,
});
await transactionalEntityManager.insert(MiUserProfile, {
userId: account.id,
autoAcceptFollowed: false,
password: hash,
});
await transactionalEntityManager.insert(MiUsedUsername, {
createdAt: new Date(),
username: username.toLowerCase(),
});
});
return account;
}
}

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm'; import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js'; import type { FollowingsRepository, MiMeta, MiUser, UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@@ -13,10 +13,14 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable() @Injectable()
export class DeleteAccountService { export class DeleteAccountService {
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -28,6 +32,7 @@ export class DeleteAccountService {
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private systemAccountService: SystemAccountService,
) { ) {
} }
@@ -36,8 +41,13 @@ export class DeleteAccountService {
id: string; id: string;
host: string | null; host: string | null;
}, moderator?: MiUser): Promise<void> { }, moderator?: MiUser): Promise<void> {
if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account');
const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
if (user.host === null && _user.username.includes('.')) {
throw new Error('cannot delete a system account');
}
if (moderator != null) { if (moderator != null) {
this.moderationLogService.log(moderator, 'deleteAccount', { this.moderationLogService.log(moderator, 'deleteAccount', {

View File

@@ -1,57 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser } from '@/models/User.js';
import type { UsersRepository } from '@/models/_.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: MemorySingleCache<MiLocalUser>;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
}
@bindThis
public async realLocalUsersPresent(): Promise<boolean> {
return await this.usersRepository.existsBy({
host: IsNull(),
username: Not(ACTOR_USERNAME),
});
}
@bindThis
public async getInstanceActor(): Promise<MiLocalUser> {
const cached = this.cache.get();
if (cached) return cached;
const user = await this.usersRepository.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
}) as MiLocalUser | undefined;
if (user) {
this.cache.set(user);
return user;
} else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as MiLocalUser;
this.cache.set(created);
return created;
}
}
}

View File

@@ -53,7 +53,7 @@ export class MetaService implements OnApplicationShutdown {
case 'metaUpdated': { case 'metaUpdated': {
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...(body.after), ...(body.after),
proxyAccount: null, // joinなカラムは通常取ってこないので rootUser: null, // joinなカラムは通常取ってこないので
}; };
break; break;
} }
@@ -113,17 +113,20 @@ export class MetaService implements OnApplicationShutdown {
if (before) { if (before) {
await transactionalEntityManager.update(MiMeta, before.id, data); await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
id: 'DESC',
},
});
return metas[0];
} else { } else {
return await transactionalEntityManager.save(MiMeta, data); await transactionalEntityManager.save(MiMeta, {
...data,
id: 'x',
});
} }
const afters = await transactionalEntityManager.find(MiMeta, {
order: {
id: 'DESC',
},
});
return afters[0];
}); });
if (data.hiddenTags) { if (data.hiddenTags) {

View File

@@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ProxyAccountService {
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
) {
}
@bindThis
public async fetch(): Promise<MiLocalUser | null> {
if (this.meta.proxyAccountId == null) return null;
return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
}
}

View File

@@ -4,53 +4,34 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import type { MiUser } from '@/models/User.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { RelaysRepository } from '@/models/_.js';
import type { RelaysRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MemorySingleCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
import type { MiRelay } from '@/models/Relay.js'; import type { MiRelay } from '@/models/Relay.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable() @Injectable()
export class RelayService { export class RelayService {
private relaysCache: MemorySingleCache<MiRelay[]>; private relaysCache: MemorySingleCache<MiRelay[]>;
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.relaysRepository) @Inject(DI.relaysRepository)
private relaysRepository: RelaysRepository, private relaysRepository: RelaysRepository,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private createSystemUserService: CreateSystemUserService, private systemAccountService: SystemAccountService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
) { ) {
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
} }
@bindThis
private async getRelayActor(): Promise<MiLocalUser> {
const user = await this.usersRepository.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
});
if (user) return user as MiLocalUser;
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
return created as MiLocalUser;
}
@bindThis @bindThis
public async addRelay(inbox: string): Promise<MiRelay> { public async addRelay(inbox: string): Promise<MiRelay> {
const relay = await this.relaysRepository.insertOne({ const relay = await this.relaysRepository.insertOne({
@@ -59,8 +40,8 @@ export class RelayService {
status: 'requesting', status: 'requesting',
}); });
const relayActor = await this.getRelayActor(); const relayActor = await this.systemAccountService.fetch('relay');
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
@@ -77,7 +58,7 @@ export class RelayService {
throw new Error('relay not found'); throw new Error('relay not found');
} }
const relayActor = await this.getRelayActor(); const relayActor = await this.systemAccountService.fetch('relay');
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);

View File

@@ -101,7 +101,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit { export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>; private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService; private notificationService: NotificationService;
@@ -137,7 +136,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
) { ) {
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@@ -406,15 +404,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> { public async isModerator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false; if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
} }
@bindThis @bindThis
public async isAdministrator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> { public async isAdministrator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false; if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
} }
@bindThis @bindThis
@@ -463,16 +461,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
.map(a => a.userId), .map(a => a.userId),
); );
if (includeRoot) { if (includeRoot && this.meta.rootUserId) {
const rootUserId = await this.rootUserIdCache.fetch(async () => { resultSet.add(this.meta.rootUserId);
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
} }
return [...resultSet].sort((x, y) => x.localeCompare(y)); return [...resultSet].sort((x, y) => x.localeCompare(y));

View File

@@ -14,13 +14,14 @@ import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js'; import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js'; import { generateNativeUserToken } from '@/misc/token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserService } from '@/core/UserService.js'; import { UserService } from '@/core/UserService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable() @Injectable()
export class SignupService { export class SignupService {
@@ -41,7 +42,8 @@ export class SignupService {
private userService: UserService, private userService: UserService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private instanceActorService: InstanceActorService, private systemAccountService: SystemAccountService,
private metaService: MetaService,
private usersChart: UsersChart, private usersChart: UsersChart,
) { ) {
} }
@@ -74,7 +76,7 @@ export class SignupService {
} }
// Generate secret // Generate secret
const secret = generateUserToken(); const secret = generateNativeUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
@@ -86,9 +88,7 @@ export class SignupService {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
} }
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); if (!opts.ignorePreservedUsernames && this.meta.rootUserId != null) {
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
@@ -129,7 +129,6 @@ export class SignupService {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host), host: this.utilityService.toPunyNullable(host),
token: secret, token: secret,
isRoot: isTheFirstUser,
})); }));
await transactionalEntityManager.save(new MiUserKeypair({ await transactionalEntityManager.save(new MiUserKeypair({
@@ -153,6 +152,10 @@ export class SignupService {
this.usersChart.update(account, true); this.usersChart.update(account, true);
this.userService.notifySystemWebhook(account, 'userCreated'); this.userService.notifySystemWebhook(account, 'userCreated');
if (this.meta.rootUserId == null) {
await this.metaService.update({ rootUserId: account.id });
}
return { account, secret }; return { account, secret };
} }
} }

View File

@@ -0,0 +1,172 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
import bcrypt from 'bcryptjs';
import { MiLocalUser, MiUser } from '@/models/User.js';
import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js';
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { generateNativeUserToken } from '@/misc/token.js';
import { IdService } from '@/core/IdService.js';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const;
@Injectable()
export class SystemAccountService {
private cache: MemoryKVCache<MiLocalUser>;
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.systemAccountsRepository)
private systemAccountsRepository: SystemAccountsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private idService: IdService,
) {
this.cache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 10); // 10m
}
@bindThis
public async list(): Promise<MiSystemAccount[]> {
const accounts = await this.systemAccountsRepository.findBy({});
return accounts;
}
@bindThis
public async fetch(type: typeof SYSTEM_ACCOUNT_TYPES[number]): Promise<MiLocalUser> {
const cached = this.cache.get(type);
if (cached) return cached;
const systemAccount = await this.systemAccountsRepository.findOne({
where: { type: type },
relations: ['user'],
});
if (systemAccount) {
this.cache.set(type, systemAccount.user as MiLocalUser);
return systemAccount.user as MiLocalUser;
} else {
const created = await this.createCorrespondingUser(type, {
username: `system.${type}`, // NOTE: (できれば避けたいが) . が含まれるかどうかでシステムアカウントかどうかを判定している処理もあるので変えないように
name: this.meta.name,
});
this.cache.set(type, created);
return created;
}
}
@bindThis
private async createCorrespondingUser(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
username: MiUser['username'];
name?: MiUser['name'];
}): Promise<MiLocalUser> {
const password = randomUUID();
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair();
let account!: MiUser;
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: extra.username.toLowerCase(),
host: IsNull(),
});
if (exist) {
account = exist;
return;
}
account = await transactionalEntityManager.insert(MiUser, {
id: this.idService.gen(),
username: extra.username,
usernameLower: extra.username.toLowerCase(),
host: null,
token: secret,
isLocked: true,
isExplorable: false,
isBot: true,
name: extra.name,
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id,
});
await transactionalEntityManager.insert(MiUserProfile, {
userId: account.id,
autoAcceptFollowed: false,
password: hash,
});
await transactionalEntityManager.insert(MiUsedUsername, {
createdAt: new Date(),
username: extra.username.toLowerCase(),
});
await transactionalEntityManager.insert(MiSystemAccount, {
id: this.idService.gen(),
userId: account.id,
type: type,
});
});
return account as MiLocalUser;
}
@bindThis
public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
name?: string;
description?: MiUserProfile['description'];
}): Promise<MiLocalUser> {
const user = await this.fetch(type);
const updates = {} as Partial<MiUser>;
if (extra.name !== undefined) updates.name = extra.name;
if (Object.keys(updates).length > 0) {
await this.usersRepository.update(user.id, updates);
}
const profileUpdates = {} as Partial<MiUserProfile>;
if (extra.description !== undefined) profileUpdates.description = extra.description;
if (Object.keys(profileUpdates).length > 0) {
await this.userProfilesRepository.update(user.id, profileUpdates);
}
const updated = await this.usersRepository.findOneByOrFail({ id: user.id }) as MiLocalUser;
this.cache.set(type, updated);
return updated;
}
}

View File

@@ -15,11 +15,11 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js'; import { RedisKVCache } from '@/misc/cache.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable() @Injectable()
export class UserListService implements OnApplicationShutdown, OnModuleInit { export class UserListService implements OnApplicationShutdown, OnModuleInit {
@@ -43,8 +43,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private queueService: QueueService, private queueService: QueueService,
private systemAccountService: SystemAccountService,
) { ) {
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', { this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
@@ -111,10 +111,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (this.userEntityService.isRemoteUser(target)) { if (this.userEntityService.isRemoteUser(target)) {
const proxy = await this.proxyAccountService.fetch(); const proxy = await this.systemAccountService.fetch('proxy');
if (proxy) { this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
}
} }
} }

View File

@@ -73,7 +73,6 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isLocked: false, isLocked: false,
isBot: false, isBot: false,
isCat: true, isCat: true,
isRoot: false,
isExplorable: true, isExplorable: true,
isHibernated: false, isHibernated: false,
isDeleted: false, isDeleted: false,

View File

@@ -507,19 +507,12 @@ export class ApInboxService {
return `skip: delete actor ${actor.uri} !== ${uri}`; return `skip: delete actor ${actor.uri} !== ${uri}`;
} }
const user = await this.usersRepository.findOneBy({ id: actor.id }); if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
if (user == null) { return 'skip: already deleted or actor not found';
return 'skip: actor not found';
} else if (user.isDeleted) {
return 'skip: already deleted';
} }
const job = await this.queueService.createDeleteAccountJob(actor); const job = await this.queueService.createDeleteAccountJob(actor);
await this.usersRepository.update(actor.id, {
isDeleted: true,
});
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id }); this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
return `ok: queued ${job.name} ${job.id}`; return `ok: queued ${job.name} ${job.id}`;

View File

@@ -23,7 +23,7 @@ import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, MiMeta } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@@ -39,6 +39,9 @@ export class ApRendererService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -186,7 +189,7 @@ export class ApRendererService {
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
}, },
_misskey_license: { _misskey_license: {
freeText: emoji.license freeText: emoji.license,
}, },
}; };
} }
@@ -255,6 +258,38 @@ export class ApRendererService {
}; };
} }
@bindThis
public renderIdenticon(user: MiLocalUser): IApImage {
return {
type: 'Image',
url: this.userEntityService.getIdenticonUrl(user),
sensitive: false,
name: null,
};
}
@bindThis
public renderSystemAvatar(user: MiLocalUser): IApImage {
if (this.meta.iconUrl == null) return this.renderIdenticon(user);
return {
type: 'Image',
url: this.meta.iconUrl,
sensitive: false,
name: null,
};
}
@bindThis
public renderSystemBanner(): IApImage | null {
if (this.meta.bannerUrl == null) return null;
return {
type: 'Image',
url: this.meta.bannerUrl,
sensitive: false,
name: null,
};
}
@bindThis @bindThis
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
return { return {
@@ -503,8 +538,8 @@ export class ApRendererService {
_misskey_requireSigninToViewContents: user.requireSigninToViewContents, _misskey_requireSigninToViewContents: user.requireSigninToViewContents,
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore, _misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore, _misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
icon: avatar ? this.renderImage(avatar) : null, icon: avatar ? this.renderImage(avatar) : isSystem ? this.renderSystemAvatar(user) : this.renderIdenticon(user),
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : isSystem ? this.renderSystemBanner() : null,
tag, tag,
manuallyApprovesFollowers: user.isLocked, manuallyApprovesFollowers: user.isLocked,
discoverable: user.isExplorable, discoverable: user.isExplorable,

View File

@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm'; import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -15,13 +14,14 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isCollectionOrOrderedCollection } from './type.js'; import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js'; import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.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'; import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js';
export class Resolver { export class Resolver {
private history: Set<string>; private history: Set<string>;
@@ -37,7 +37,7 @@ export class Resolver {
private noteReactionsRepository: NoteReactionsRepository, private noteReactionsRepository: NoteReactionsRepository,
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService, private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@@ -105,7 +105,7 @@ export class Resolver {
} }
if (this.config.signToActivityPubGet && !this.user) { if (this.config.signToActivityPubGet && !this.user) {
this.user = await this.instanceActorService.getInstanceActor(); this.user = await this.systemAccountService.fetch('actor');
} }
const object = (this.user const object = (this.user
@@ -119,7 +119,7 @@ export class Resolver {
) { ) {
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response'); throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
} }
return object; return object;
} }
@@ -202,7 +202,7 @@ export class ApResolverService {
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService, private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@@ -222,7 +222,7 @@ export class ApResolverService {
this.noteReactionsRepository, this.noteReactionsRepository,
this.followRequestsRepository, this.followRequestsRepository,
this.utilityService, this.utilityService,
this.instanceActorService, this.systemAccountService,
this.apRequestService, this.apRequestService,
this.httpRequestService, this.httpRequestService,
this.apRendererService, this.apRendererService,

View File

@@ -594,7 +594,9 @@ export class ApPersonService implements OnModuleInit {
if (moving) updates.movedAt = new Date(); if (moving) updates.movedAt = new Date();
// Update user // Update user
await this.usersRepository.update(exist.id, updates); if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) {
return 'skip';
}
if (person.publicKey) { if (person.publicKey) {
await this.userPublickeysRepository.update({ userId: exist.id }, { await this.userPublickeysRepository.update({ userId: exist.id }, {
@@ -699,7 +701,7 @@ export class ApPersonService implements OnModuleInit {
@bindThis @bindThis
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> { public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false });
if (!this.userEntityService.isRemoteUser(user)) return; if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;

View File

@@ -11,8 +11,7 @@ import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js'; import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@@ -29,8 +28,7 @@ export class MetaEntityService {
@Inject(DI.adsRepository) @Inject(DI.adsRepository)
private adsRepository: AdsRepository, private adsRepository: AdsRepository,
private userEntityService: UserEntityService, private systemAccountService: SystemAccountService,
private instanceActorService: InstanceActorService,
) { } ) { }
@bindThis @bindThis
@@ -149,14 +147,14 @@ export class MetaEntityService {
const packed = await this.pack(instance); const packed = await this.pack(instance);
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; const proxyAccount = await this.systemAccountService.fetch('proxy');
const packDetailed: Packed<'MetaDetailed'> = { const packDetailed: Packed<'MetaDetailed'> = {
...packed, ...packed,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(), requireSetup: this.meta.rootUserId == null,
proxyAccountName: proxyAccount ? proxyAccount.username : null, proxyAccountName: proxyAccount.username,
features: { features: {
localTimeline: instance.policies.ltlAvailable, localTimeline: instance.policies.ltlAvailable,
globalTimeline: instance.policies.gtlAvailable, globalTimeline: instance.policies.gtlAvailable,

View File

@@ -28,6 +28,7 @@ import type {
FollowingsRepository, FollowingsRepository,
FollowRequestsRepository, FollowRequestsRepository,
MiFollowing, MiFollowing,
MiMeta,
MiUserNotePining, MiUserNotePining,
MiUserProfile, MiUserProfile,
MutingsRepository, MutingsRepository,
@@ -100,6 +101,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@@ -381,7 +385,11 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public getIdenticonUrl(user: MiUser): string { public getIdenticonUrl(user: MiUser): string {
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`; if ((user.host == null || user.host === this.config.host) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合
return this.meta.iconUrl;
} else {
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
}
} }
@bindThis @bindThis

View File

@@ -74,6 +74,7 @@ export const DI = {
registryItemsRepository: Symbol('registryItemsRepository'), registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'), webhooksRepository: Symbol('webhooksRepository'),
systemWebhooksRepository: Symbol('systemWebhooksRepository'), systemWebhooksRepository: Symbol('systemWebhooksRepository'),
systemAccountsRepository: Symbol('systemAccountsRepository'),
adsRepository: Symbol('adsRepository'), adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),

View File

@@ -1,7 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// eslint-disable-next-line import/no-default-export
export default (token: string) => token.length === 16;

View File

@@ -5,5 +5,6 @@
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
// eslint-disable-next-line import/no-default-export export const generateNativeUserToken = () => secureRndstr(16);
export default () => secureRndstr(16);
export const isNativeUserToken = (token: string) => token.length === 16;

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
@@ -15,6 +15,18 @@ export class MiMeta {
}) })
public id: string; public id: string;
@Column({
...id(),
nullable: true,
})
public rootUserId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
public rootUser: MiUser | null;
@Column('varchar', { @Column('varchar', {
length: 1024, nullable: true, length: 1024, nullable: true,
}) })
@@ -172,18 +184,6 @@ export class MiMeta {
}) })
public cacheRemoteSensitiveFiles: boolean; public cacheRemoteSensitiveFiles: boolean;
@Column({
...id(),
nullable: true,
})
public proxyAccountId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
public proxyAccount: MiUser | null;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { import {
@@ -63,6 +62,7 @@ import {
MiRoleAssignment, MiRoleAssignment,
MiSignin, MiSignin,
MiSwSubscription, MiSwSubscription,
MiSystemAccount,
MiSystemWebhook, MiSystemWebhook,
MiUsedUsername, MiUsedUsername,
MiUser, MiUser,
@@ -77,8 +77,9 @@ import {
MiUserProfile, MiUserProfile,
MiUserPublickey, MiUserPublickey,
MiUserSecurityKey, MiUserSecurityKey,
MiWebhook MiWebhook,
} from './_.js'; } from './_.js';
import type { Provider } from '@nestjs/common';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
const $usersRepository: Provider = { const $usersRepository: Provider = {
@@ -285,6 +286,12 @@ const $swSubscriptionsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $systemAccountsRepository: Provider = {
provide: DI.systemAccountsRepository,
useFactory: (db: DataSource) => db.getRepository(MiSystemAccount),
inject: [DI.db],
};
const $hashtagsRepository: Provider = { const $hashtagsRepository: Provider = {
provide: DI.hashtagsRepository, provide: DI.hashtagsRepository,
useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>), useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>),
@@ -532,6 +539,7 @@ const $reversiGamesRepository: Provider = {
$renoteMutingsRepository, $renoteMutingsRepository,
$blockingsRepository, $blockingsRepository,
$swSubscriptionsRepository, $swSubscriptionsRepository,
$systemAccountsRepository,
$hashtagsRepository, $hashtagsRepository,
$abuseUserReportsRepository, $abuseUserReportsRepository,
$abuseReportNotificationRecipientRepository, $abuseReportNotificationRecipientRepository,
@@ -603,6 +611,7 @@ const $reversiGamesRepository: Provider = {
$renoteMutingsRepository, $renoteMutingsRepository,
$blockingsRepository, $blockingsRepository,
$swSubscriptionsRepository, $swSubscriptionsRepository,
$systemAccountsRepository,
$hashtagsRepository, $hashtagsRepository,
$abuseUserReportsRepository, $abuseUserReportsRepository,
$abuseReportNotificationRecipientRepository, $abuseReportNotificationRecipientRepository,

View File

@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { Serialized } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('system_account')
@Index(['type'], { unique: true })
export class MiSystemAccount {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Column('varchar', {
length: 256,
})
public type: string;
}

View File

@@ -184,12 +184,6 @@ export class MiUser {
}) })
public isCat: boolean; public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the root.',
})
public isRoot: boolean;
@Index() @Index()
@Column('boolean', { @Column('boolean', {
default: true, default: true,

View File

@@ -56,6 +56,7 @@ import { MiRegistryItem } from '@/models/RegistryItem.js';
import { MiRelay } from '@/models/Relay.js'; import { MiRelay } from '@/models/Relay.js';
import { MiSignin } from '@/models/Signin.js'; import { MiSignin } from '@/models/Signin.js';
import { MiSwSubscription } from '@/models/SwSubscription.js'; import { MiSwSubscription } from '@/models/SwSubscription.js';
import { MiSystemAccount } from '@/models/SystemAccount.js';
import { MiUsedUsername } from '@/models/UsedUsername.js'; import { MiUsedUsername } from '@/models/UsedUsername.js';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
import { MiUserIp } from '@/models/UserIp.js'; import { MiUserIp } from '@/models/UserIp.js';
@@ -171,6 +172,7 @@ export {
MiRelay, MiRelay,
MiSignin, MiSignin,
MiSwSubscription, MiSwSubscription,
MiSystemAccount,
MiUsedUsername, MiUsedUsername,
MiUser, MiUser,
MiUserIp, MiUserIp,
@@ -242,6 +244,7 @@ export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<
export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>; export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>;
export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>; export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>; export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
export type SystemAccountsRepository = Repository<MiSystemAccount> & MiRepository<MiSystemAccount>;
export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>; export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>; export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>; export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;

View File

@@ -82,6 +82,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
import { Config } from '@/config.js'; import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js'; import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MiSystemAccount } from './models/SystemAccount.js';
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
@@ -206,6 +207,7 @@ export const entities = [
MiEmoji, MiEmoji,
MiHashtag, MiHashtag,
MiSwSubscription, MiSwSubscription,
MiSystemAccount,
MiAbuseUserReport, MiAbuseUserReport,
MiAbuseReportNotificationRecipient, MiAbuseReportNotificationRecipient,
MiRegistrationTicket, MiRegistrationTicket,

View File

@@ -9,11 +9,11 @@ import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MemorySingleCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js'; import NotesChart from '@/core/chart/charts/notes.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
const nodeinfo2_1path = '/nodeinfo/2.1'; const nodeinfo2_1path = '/nodeinfo/2.1';
@@ -26,7 +26,7 @@ export class NodeinfoServerService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
private userEntityService: UserEntityService, private systemAccountService: SystemAccountService,
private metaService: MetaService, private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private usersChart: UsersChart, private usersChart: UsersChart,
@@ -70,7 +70,7 @@ export class NodeinfoServerService {
const activeHalfyear = null; const activeHalfyear = null;
const activeMonth = null; const activeMonth = null;
const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; const proxyAccount = await this.systemAccountService.fetch('proxy');
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
@@ -123,7 +123,7 @@ export class NodeinfoServerService {
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
enableEmail: meta.enableEmail, enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker, enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null, proxyAccountName: proxyAccount.username,
themeColor: meta.themeColor ?? '#86b300', themeColor: meta.themeColor ?? '#86b300',
}, },
}; };

View File

@@ -371,7 +371,7 @@ export class ApiCallService implements OnApplicationShutdown {
} }
} }
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id); const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
@@ -391,7 +391,7 @@ export class ApiCallService implements OnApplicationShutdown {
} }
} }
if (ep.meta.requireRolePolicy != null && !user!.isRoot) { if (ep.meta.requireRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id); const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id); const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) { if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {

View File

@@ -11,7 +11,7 @@ import type { MiAccessToken } from '@/models/AccessToken.js';
import { MemoryKVCache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import isNativeToken from '@/misc/is-native-token.js'; import { isNativeUserToken } from '@/misc/token.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
export class AuthenticationError extends Error { export class AuthenticationError extends Error {
@@ -46,7 +46,7 @@ export class AuthenticateService implements OnApplicationShutdown {
return [null, null]; return [null, null];
} }
if (isNativeToken(token)) { if (isNativeUserToken(token)) {
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>); () => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>);

View File

@@ -100,6 +100,7 @@ export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.
export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js'; export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';
export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js'; export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js';
export * as 'admin/update-meta' from './endpoints/admin/update-meta.js'; export * as 'admin/update-meta' from './endpoints/admin/update-meta.js';
export * as 'admin/update-proxy-account' from './endpoints/admin/update-proxy-account.js';
export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js'; export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js';
export * as 'announcements' from './endpoints/announcements.js'; export * as 'announcements' from './endpoints/announcements.js';
export * as 'announcements/show' from './endpoints/announcements/show.js'; export * as 'announcements/show' from './endpoints/announcements/show.js';

View File

@@ -4,12 +4,10 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { MiMeta, UsersRepository } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js'; import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@@ -62,18 +60,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private signupService: SignupService, private signupService: SignupService,
private instanceActorService: InstanceActorService,
) { ) {
super(meta, paramDef, async (ps, _me, token) => { super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const realUsers = await this.instanceActorService.realLocalUsersPresent();
if (!realUsers && me == null && token == null) { if (this.serverSettings.rootUserId == null && me == null && token == null) {
// 初回セットアップの場合 // 初回セットアップの場合
if (this.config.setupPassword != null) { if (this.config.setupPassword != null) {
// 初期パスワードが設定されている場合 // 初期パスワードが設定されている場合
@@ -85,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// 初期パスワードが設定されていないのに初期パスワードが入力された場合 // 初期パスワードが設定されていないのに初期パスワードが入力された場合
throw new ApiError(meta.errors.wrongInitialPassword); throw new ApiError(meta.errors.wrongInitialPassword);
} }
} else if ((realUsers && !me?.isRoot) || token !== null) { } else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) {
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合 // 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
throw new ApiError(meta.errors.accessDenied); throw new ApiError(meta.errors.accessDenied);
} }

View File

@@ -42,10 +42,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found'); throw new Error('user not found');
} }
if (user.isRoot) {
throw new Error('cannot delete a root account');
}
await this.deleteAccoountService.deleteAccount(user, me); await this.deleteAccoountService.deleteAccount(user, me);
}); });
} }

View File

@@ -9,6 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@@ -237,7 +238,7 @@ export const meta = {
}, },
proxyAccountId: { proxyAccountId: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: false,
format: 'id', format: 'id',
}, },
email: { email: {
@@ -545,10 +546,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private config: Config, private config: Config,
private metaService: MetaService, private metaService: MetaService,
private systemAccountService: SystemAccountService,
) { ) {
super(meta, paramDef, async () => { super(meta, paramDef, async () => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
const proxy = await this.systemAccountService.fetch('proxy');
return { return {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,
@@ -613,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId, proxyAccountId: proxy.id,
email: instance.email, email: instance.email,
smtpSecure: instance.smtpSecure, smtpSecure: instance.smtpSecure,
smtpHost: instance.smtpHost, smtpHost: instance.smtpHost,

View File

@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -43,6 +43,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -58,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found'); throw new Error('user not found');
} }
if (user.isRoot) { if (this.serverSettings.rootUserId === user.id) {
throw new Error('cannot reset password of root'); throw new Error('cannot reset password of root');
} }

View File

@@ -89,7 +89,6 @@ export const paramDef = {
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' }, setSensitiveFlagAutomatically: { type: 'boolean' },
enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
maintainerName: { type: 'string', nullable: true }, maintainerName: { type: 'string', nullable: true },
maintainerEmail: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true },
langs: { langs: {
@@ -394,10 +393,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
} }
if (ps.proxyAccountId !== undefined) {
set.proxyAccountId = ps.proxyAccountId;
}
if (ps.maintainerName !== undefined) { if (ps.maintainerName !== undefined) {
set.maintainerName = ps.maintainerName; set.maintainerName = ps.maintainerName;
} }

View File

@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import {
descriptionSchema,
} from '@/models/User.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:account',
res: {
type: 'object',
nullable: false, optional: false,
ref: 'UserDetailed',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
description: { ...descriptionSchema, nullable: true },
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private userEntityService: UserEntityService,
private moderationLogService: ModerationLogService,
private systemAccountService: SystemAccountService,
) {
super(meta, paramDef, async (ps, me) => {
const proxy = await this.systemAccountService.updateCorrespondingUserProfile('proxy', {
description: ps.description,
});
const updated = await this.userEntityService.pack(proxy.id, proxy, {
schema: 'MeDetailed',
});
if (ps.description !== undefined) {
this.moderationLogService.log(me, 'updateProxyAccountDescription', {
before: null, //TODO
after: ps.description,
});
}
return updated;
});
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -19,6 +19,8 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { DI } from '@/di-symbols.js';
import { MiMeta } from '@/models/_.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@@ -81,6 +83,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private remoteUserResolveService: RemoteUserResolveService, private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService, private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService, private accountMoveService: AccountMoveService,
@@ -92,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// check parameter // check parameter
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser); if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
// abort if user is the root // abort if user is the root
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden); if (this.serverSettings.rootUserId === me.id) throw new ApiError(meta.errors.rootForbidden);
// abort if user has already moved // abort if user has already moved
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved); if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);

View File

@@ -7,7 +7,7 @@ import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js'; import { generateNativeUserToken } from '@/misc/token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('incorrect password'); throw new Error('incorrect password');
} }
const newToken = generateUserToken(); const newToken = generateNativeUserToken();
await this.usersRepository.update(me.id, { await this.usersRepository.update(me.id, {
token: newToken, token: newToken,

View File

@@ -6,9 +6,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { LoggerService } from '@/core/LoggerService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { resetDb } from '@/misc/reset-db.js'; import { resetDb } from '@/misc/reset-db.js';
import { MetaService } from '@/core/MetaService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = { export const meta = {
tags: ['non-productive'], tags: ['non-productive'],
@@ -36,13 +39,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
private loggerService: LoggerService,
private metaService: MetaService,
private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
await redisClient.flushdb(); const logger = this.loggerService.getLogger('reset-db');
logger.info('---- Resetting database...');
await this.redisClient.flushdb();
await resetDb(this.db); await resetDb(this.db);
// DIコンテナで管理しているmetaのインスタンスには上記のリセット処理が届かないため、
// 初期値を流して明示的にリフレッシュする
const meta = await this.metaService.fetch(true);
this.globalEventService.publishInternalEvent('metaUpdated', { after: meta });
logger.info('---- Database reset complete.');
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
}); });
} }

View File

@@ -122,6 +122,7 @@ export const moderationLogTypes = [
'deletePage', 'deletePage',
'deleteFlash', 'deleteFlash',
'deleteGalleryPost', 'deleteGalleryPost',
'updateProxyAccountDescription',
] as const; ] as const;
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
@@ -374,25 +375,29 @@ export type ModerationLogPayloads = {
postUserUsername: string; postUserUsername: string;
post: any; post: any;
}; };
updateProxyAccountDescription: {
before: string | null;
after: string | null;
};
}; };
export type Serialized<T> = { export type Serialized<T> = {
[K in keyof T]: [K in keyof T]:
T[K] extends Date T[K] extends Date
? string ? string
: T[K] extends (Date | null) : T[K] extends (Date | null)
? (string | null) ? (string | null)
: T[K] extends Record<string, any> : T[K] extends Record<string, any>
? Serialized<T[K]> ? Serialized<T[K]>
: T[K] extends (Record<string, any> | null) : T[K] extends (Record<string, any> | null)
? (Serialized<T[K]> | null) ? (Serialized<T[K]> | null)
: T[K] extends (Record<string, any> | undefined) : T[K] extends (Record<string, any> | undefined)
? (Serialized<T[K]> | undefined) ? (Serialized<T[K]> | undefined)
: T[K]; : T[K];
}; };
export type FilterUnionByProperty< export type FilterUnionByProperty<
Union, Union,
Property extends string | number | symbol, Property extends string | number | symbol,
Condition Condition,
> = Union extends Record<Property, Condition> ? Union : never; > = Union extends Record<Property, Condition> ? Union : never;

View File

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

View File

@@ -16,12 +16,16 @@ services:
" "
tester: tester:
image: node:20 image: node:22.14.0
depends_on: depends_on:
a.test: a.test:
condition: service_healthy condition: service_healthy
misskey.a.test:
condition: service_healthy
b.test: b.test:
condition: service_healthy condition: service_healthy
misskey.b.test:
condition: service_healthy
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
@@ -82,7 +86,7 @@ services:
command: pnpm -F backend test:fed command: pnpm -F backend test:fed
daemon: daemon:
image: node:20 image: node:22.14.0
depends_on: depends_on:
redis.test: redis.test:
condition: service_healthy condition: service_healthy

View File

@@ -35,7 +35,7 @@ describe('Abuse report', () => {
const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {}); const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0]; const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
// NOTE: reporter is not Alice, and is not moderator in A // NOTE: reporter is not Alice, and is not moderator in A
strictEqual(reportInB.reporter.url, 'https://a.test/@instance.actor'); strictEqual(reportInB.reporter.url, 'https://a.test/@system.actor');
strictEqual(reportInB.targetUserId, bob.id); strictEqual(reportInB.targetUserId, bob.id);
// NOTE: cannot forward multiple times // NOTE: cannot forward multiple times

View File

@@ -37,6 +37,7 @@ describe('User', () => {
'id', 'id',
'host', 'host',
'avatarUrl', 'avatarUrl',
'avatarBlurhash',
'instance', 'instance',
'badgeRoles', 'badgeRoles',
'url', 'url',
@@ -379,7 +380,8 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob strictEqual(followers.length, 1); // followed by Bob
await alice.client.request('i/delete-account', { password: alice.password }); await alice.client.request('i/delete-account', { password: alice.password });
await sleep(); // NOTE: user deletion query is slow
await sleep(4000);
const following = await bob.client.request('users/following', { userId: bob.id }); const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation strictEqual(following.length, 0); // no following relation
@@ -477,7 +479,8 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob strictEqual(followers.length, 1); // followed by Bob
await aAdmin.client.request('admin/suspend-user', { userId: alice.id }); await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
await sleep(); // NOTE: user deletion query is slow
await sleep(4000);
const following = await bob.client.request('users/following', { userId: bob.id }); const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation strictEqual(following.length, 0); // no following relation

View File

@@ -36,7 +36,7 @@ export type Request = <
type Host = 'a.test' | 'b.test'; type Host = 'a.test' | 'b.test';
export async function sleep(ms = 200): Promise<void> { export async function sleep(ms = 250): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }

View File

@@ -7,14 +7,10 @@ import type { Config } from '@/config.js';
import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import type { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import type { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import type { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import type { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { Resolver } from '@/core/activitypub/ApResolverService.js';
import type { IObject } from '@/core/activitypub/type.js'; import type { IObject } from '@/core/activitypub/type.js';
import type { HttpRequestService } from '@/core/HttpRequestService.js'; import type { HttpRequestService } from '@/core/HttpRequestService.js';
import type { InstanceActorService } from '@/core/InstanceActorService.js';
import type { LoggerService } from '@/core/LoggerService.js'; import type { LoggerService } from '@/core/LoggerService.js';
import type { MetaService } from '@/core/MetaService.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { import type {
FollowRequestsRepository, FollowRequestsRepository,
MiMeta, MiMeta,
@@ -23,6 +19,9 @@ import type {
PollsRepository, PollsRepository,
UsersRepository, UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { bindThis } from '@/decorators.js';
import { Resolver } from '@/core/activitypub/ApResolverService.js';
type MockResponse = { type MockResponse = {
type: string; type: string;
@@ -43,7 +42,7 @@ export class MockResolver extends Resolver {
{} as NoteReactionsRepository, {} as NoteReactionsRepository,
{} as FollowRequestsRepository, {} as FollowRequestsRepository,
{} as UtilityService, {} as UtilityService,
{} as InstanceActorService, {} as SystemAccountService,
{} as ApRequestService, {} as ApRequestService,
{} as HttpRequestService, {} as HttpRequestService,
{} as ApRendererService, {} as ApRendererService,

View File

@@ -149,9 +149,9 @@ describe('AbuseReportNotificationService', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); root = await createUser({ username: 'root', usernameLower: 'root' });
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); alice = await createUser({ username: 'alice', usernameLower: 'alice' });
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false }); bob = await createUser({ username: 'bob', usernameLower: 'bob' });
systemWebhook1 = await createWebhook(); systemWebhook1 = await createWebhook();
systemWebhook2 = await createWebhook(); systemWebhook2 = await createWebhook();

View File

@@ -79,9 +79,9 @@ describe('FlashService', () => {
userProfilesRepository = app.get(DI.userProfilesRepository); userProfilesRepository = app.get(DI.userProfilesRepository);
idService = app.get(IdService); idService = app.get(IdService);
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); root = await createUser({ username: 'root', usernameLower: 'root' });
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); alice = await createUser({ username: 'alice', usernameLower: 'alice' });
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false }); bob = await createUser({ username: 'bob', usernameLower: 'bob' });
}); });
afterEach(async () => { afterEach(async () => {

View File

@@ -3,24 +3,21 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { UtilityService } from '@/core/UtilityService.js';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js'; import { ModuleMocker } from 'jest-mock';
import { RelayService } from '@/core/RelayService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
import { IdService } from '@/core/IdService.js';
import type { RelaysRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { RelayService } from '@/core/RelayService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { UtilityService } from '@/core/UtilityService.js';
const moduleMocker = new ModuleMocker(global); const moduleMocker = new ModuleMocker(global);
@@ -28,8 +25,6 @@ describe('RelayService', () => {
let app: TestingModule; let app: TestingModule;
let relayService: RelayService; let relayService: RelayService;
let queueService: jest.Mocked<QueueService>; let queueService: jest.Mocked<QueueService>;
let relaysRepository: RelaysRepository;
let userEntityService: UserEntityService;
beforeAll(async () => { beforeAll(async () => {
app = await Test.createTestingModule({ app = await Test.createTestingModule({
@@ -38,10 +33,10 @@ describe('RelayService', () => {
], ],
providers: [ providers: [
IdService, IdService,
CreateSystemUserService,
ApRendererService, ApRendererService,
RelayService, RelayService,
UserEntityService, UserEntityService,
SystemAccountService,
UtilityService, UtilityService,
], ],
}) })
@@ -61,8 +56,6 @@ describe('RelayService', () => {
relayService = app.get<RelayService>(RelayService); relayService = app.get<RelayService>(RelayService);
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>; queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
relaysRepository = app.get<RelaysRepository>(DI.relaysRepository);
userEntityService = app.get<UserEntityService>(UserEntityService);
}); });
afterAll(async () => { afterAll(async () => {

View File

@@ -57,6 +57,12 @@ describe('RoleService', () => {
return await usersRepository.findOneByOrFail(x.identifiers[0]); return await usersRepository.findOneByOrFail(x.identifiers[0]);
} }
async function createRoot(data: Partial<MiUser> = {}) {
const user = await createUser(data);
meta.rootUserId = user.id;
return user;
}
async function createRole(data: Partial<MiRole> = {}) { async function createRole(data: Partial<MiRole> = {}) {
const x = await rolesRepository.insert({ const x = await rolesRepository.insert({
id: genAidx(Date.now()), id: genAidx(Date.now()),
@@ -279,7 +285,7 @@ describe('RoleService', () => {
describe('getModeratorIds', () => { describe('getModeratorIds', () => {
test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => { test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -305,7 +311,7 @@ describe('RoleService', () => {
test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => { test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -331,7 +337,7 @@ describe('RoleService', () => {
test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => { test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -357,7 +363,7 @@ describe('RoleService', () => {
test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => { test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -383,7 +389,7 @@ describe('RoleService', () => {
test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => { test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -409,7 +415,7 @@ describe('RoleService', () => {
test('root has moderator role', async () => { test('root has moderator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -433,7 +439,7 @@ describe('RoleService', () => {
test('root has administrator role', async () => { test('root has administrator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -457,7 +463,7 @@ describe('RoleService', () => {
test('root has moderator role(expire)', async () => { test('root has moderator role(expire)', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }), createUser(), createUser(), createUser(), createRoot(),
]); ]);
const role1 = await createRole({ name: 'admin', isAdministrator: true }); const role1 = await createRole({ name: 'admin', isAdministrator: true });

View File

@@ -97,7 +97,7 @@ describe('SystemWebhookService', () => {
} }
async function beforeEachImpl() { async function beforeEachImpl() {
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' }); root = await createUser({ username: 'root', usernameLower: 'root' });
} }
async function afterEachImpl() { async function afterEachImpl() {

View File

@@ -113,7 +113,7 @@ describe('UserSearchService', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); root = await createUser({ username: 'root', usernameLower: 'root' });
alice = await createUser({ username: 'Alice', usernameLower: 'alice' }); alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' }); alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' }); alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });

View File

@@ -91,7 +91,7 @@ describe('UserWebhookService', () => {
} }
async function beforeEachImpl() { async function beforeEachImpl() {
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' }); root = await createUser({ username: 'root', usernameLower: 'root' });
} }
async function afterEachImpl() { async function afterEachImpl() {

View File

@@ -88,8 +88,8 @@ describe('WebhookTestService', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); root = await createUser({ username: 'root', usernameLower: 'root' });
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); alice = await createUser({ username: 'alice', usernameLower: 'alice' });
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([ userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook, { id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,

View File

@@ -316,7 +316,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
createUser({}, { email: 'user2@example.com', emailVerified: false }), createUser({}, { email: 'user2@example.com', emailVerified: false }),
createUser({}, { email: null, emailVerified: false }), createUser({}, { email: null, emailVerified: false }),
createUser({}, { email: 'user4@example.com', emailVerified: true }), createUser({}, { email: 'user4@example.com', emailVerified: true }),
createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }), createUser({}, { email: 'root@example.com', emailVerified: true }),
]); ]);
mockModeratorRole([user1, user2, user3, root]); mockModeratorRole([user1, user2, user3, root]);
@@ -349,7 +349,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
createUser({}, { email: 'user2@example.com', emailVerified: false }), createUser({}, { email: 'user2@example.com', emailVerified: false }),
createUser({}, { email: null, emailVerified: false }), createUser({}, { email: null, emailVerified: false }),
createUser({}, { email: 'user4@example.com', emailVerified: true }), createUser({}, { email: 'user4@example.com', emailVerified: true }),
createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }), createUser({}, { email: 'root@example.com', emailVerified: true }),
]); ]);
mockModeratorRole([user1, user2, user3, root]); mockModeratorRole([user1, user2, user3, root]);

View File

@@ -25,16 +25,16 @@
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.34.8", "rollup": "4.34.9",
"sass": "1.85.0", "sass": "1.85.1",
"shiki": "3.0.0", "shiki": "3.1.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.11",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.7.3", "typescript": "5.8.2",
"uuid": "11.1.0", "uuid": "11.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"vite": "6.1.1", "vite": "6.2.0",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"devDependencies": { "devDependencies": {
@@ -42,29 +42,29 @@
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "22.13.5", "@types/node": "22.13.9",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14", "@types/ws": "8.18.0",
"@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/eslint-plugin": "8.26.0",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/parser": "8.26.0",
"@vitest/coverage-v8": "3.0.6", "@vitest/coverage-v8": "3.0.7",
"@vue/runtime-core": "3.5.13", "@vue/runtime-core": "3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.33.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "17.1.4", "happy-dom": "17.2.2",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.7.1", "msw": "2.7.3",
"nodemon": "3.1.9", "nodemon": "3.1.9",
"prettier": "3.5.2", "prettier": "3.5.3",
"start-server-and-test": "2.0.10", "start-server-and-test": "2.0.10",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.2.4", "vue-component-type-helpers": "2.2.8",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4" "vue-tsc": "2.2.8"
} }
} }

View File

@@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.13.5", "@types/node": "22.13.9",
"@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/eslint-plugin": "8.26.0",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/parser": "8.26.0",
"esbuild": "0.25.0", "esbuild": "0.25.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.33.0",
"nodemon": "3.1.9", "nodemon": "3.1.9",
"typescript": "5.7.3", "typescript": "5.8.2",
"vue-eslint-parser": "9.4.3" "vue-eslint-parser": "9.4.3"
}, },
"files": [ "files": [

View File

@@ -40,9 +40,9 @@
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.25.2", "chromatic": "11.27.0",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2", "cropperjs": "2.0.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
@@ -58,84 +58,84 @@
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.34.8", "rollup": "4.34.9",
"sanitize-html": "2.14.0", "sanitize-html": "2.14.0",
"sass": "1.85.0", "sass": "1.85.1",
"shiki": "3.0.0", "shiki": "3.1.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.173.0", "three": "0.174.0",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.11",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.7.3", "typescript": "5.8.2",
"uuid": "11.1.0", "uuid": "11.1.0",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "6.1.1", "vite": "6.2.0",
"vue": "3.5.13", "vue": "3.5.13",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.0", "@misskey-dev/summaly": "5.2.0",
"@storybook/addon-actions": "8.5.8", "@storybook/addon-actions": "8.6.3",
"@storybook/addon-essentials": "8.5.8", "@storybook/addon-essentials": "8.6.3",
"@storybook/addon-interactions": "8.5.8", "@storybook/addon-interactions": "8.6.3",
"@storybook/addon-links": "8.5.8", "@storybook/addon-links": "8.6.3",
"@storybook/addon-mdx-gfm": "8.5.8", "@storybook/addon-mdx-gfm": "8.6.3",
"@storybook/addon-storysource": "8.5.8", "@storybook/addon-storysource": "8.6.3",
"@storybook/blocks": "8.5.8", "@storybook/blocks": "8.6.3",
"@storybook/components": "8.5.8", "@storybook/components": "8.6.3",
"@storybook/core-events": "8.5.8", "@storybook/core-events": "8.6.3",
"@storybook/manager-api": "8.5.8", "@storybook/manager-api": "8.6.3",
"@storybook/preview-api": "8.5.8", "@storybook/preview-api": "8.6.3",
"@storybook/react": "8.5.8", "@storybook/react": "8.6.3",
"@storybook/react-vite": "8.5.8", "@storybook/react-vite": "8.6.3",
"@storybook/test": "8.5.8", "@storybook/test": "8.6.3",
"@storybook/theming": "8.5.8", "@storybook/theming": "8.6.3",
"@storybook/types": "8.5.8", "@storybook/types": "8.6.3",
"@storybook/vue3": "8.5.8", "@storybook/vue3": "8.6.3",
"@storybook/vue3-vite": "8.5.8", "@storybook/vue3-vite": "8.6.3",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "22.13.5", "@types/node": "22.13.9",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0", "@types/sanitize-html": "2.13.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14", "@types/ws": "8.18.0",
"@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/eslint-plugin": "8.26.0",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/parser": "8.26.0",
"@vitest/coverage-v8": "3.0.6", "@vitest/coverage-v8": "3.0.7",
"@vue/runtime-core": "3.5.13", "@vue/runtime-core": "3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.0.3", "cypress": "14.1.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.33.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "17.1.4", "happy-dom": "17.2.2",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.7.1", "msw": "2.7.3",
"msw-storybook-addon": "2.0.4", "msw-storybook-addon": "2.0.4",
"nodemon": "3.1.9", "nodemon": "3.1.9",
"prettier": "3.5.2", "prettier": "3.5.3",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.0.10", "start-server-and-test": "2.0.10",
"storybook": "8.5.8", "storybook": "8.6.3",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.0.6", "vitest": "3.0.7",
"vitest-fetch-mock": "0.4.3", "vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "2.2.4", "vue-component-type-helpers": "2.2.8",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4" "vue-tsc": "2.2.8"
} }
} }

View File

@@ -413,7 +413,7 @@ function computeButtonTitle(ev: MouseEvent): void {
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) { if (el && defaultStore.state.animation) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);

View File

@@ -259,7 +259,14 @@ function showMenu(ev: MouseEvent) {
}); });
} }
function toggleSensitive(file: Misskey.entities.DriveFile) { async function toggleSensitive(file: Misskey.entities.DriveFile) {
const { canceled } = await os.confirm({
type: 'warning',
text: file.isSensitive ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
os.apiWithDialog('drive/files/update', { os.apiWithDialog('drive/files/update', {
fileId: file.id, fileId: file.id,
isSensitive: !file.isSensitive, isSensitive: !file.isSensitive,

View File

@@ -124,11 +124,21 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) { if (iAmModerator) {
menuItems.push({ menuItems.push({
text: i18n.ts.markAsSensitive, text: props.image.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: 'ti ti-eye-exclamation', icon: 'ti ti-eye-exclamation',
danger: true, danger: true,
action: () => { action: async () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); const { canceled } = await os.confirm({
type: 'warning',
text: props.image.isSensitive ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
os.apiWithDialog('drive/files/update', {
fileId: props.image.id,
isSensitive: !props.image.isSensitive,
});
}, },
}); });
} }

View File

@@ -284,7 +284,14 @@ function showMenu(ev: MouseEvent) {
}); });
} }
function toggleSensitive(file: Misskey.entities.DriveFile) { async function toggleSensitive(file: Misskey.entities.DriveFile) {
const { canceled } = await os.confirm({
type: 'warning',
text: file.isSensitive ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
os.apiWithDialog('drive/files/update', { os.apiWithDialog('drive/files/update', {
fileId: file.id, fileId: file.id,
isSensitive: !file.isSensitive, isSensitive: !file.isSensitive,

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :class="$style.icon" :src="avatarUrl" alt=""> <img :class="$style.icon" :src="avatarUrl" alt="">
<span> <span>
<span>@{{ username }}</span> <span>@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span> <span v-if="(host != localHost)" :class="$style.host">@{{ toUnicode(host) }}</span>
</span> </span>
</MkA> </MkA>
</template> </template>
@@ -17,10 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only
import { toUnicode } from 'punycode.js'; import { toUnicode } from 'punycode.js';
import { computed } from 'vue'; import { computed } from 'vue';
import { host as localHost } from '@@/js/config.js'; import { host as localHost } from '@@/js/config.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import type { MkABehavior } from '@/components/global/MkA.vue';
const props = defineProps<{ const props = defineProps<{
username: string; username: string;

View File

@@ -479,7 +479,7 @@ function react(): void {
reaction: '❤️', reaction: '❤️',
}); });
const el = reactButton.value; const el = reactButton.value;
if (el) { if (el && defaultStore.state.animation) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);

View File

@@ -442,7 +442,7 @@ function react(): void {
reaction: '❤️', reaction: '❤️',
}); });
const el = reactButton.value; const el = reactButton.value;
if (el) { if (el && defaultStore.state.animation) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);

View File

@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<div v-show="useCw" :class="$style.cwOuter"> <div v-show="useCw" :class="$style.cwOuter">
<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
<div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div> <div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div>
</div> </div>
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
@@ -104,18 +104,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue'; import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import type { ShallowRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js'; import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js'; import { host, url } from '@@/js/config.js';
import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkNotePreview from '@/components/MkNotePreview.vue'; import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor from '@/components/MkPollEditor.vue'; import MkPollEditor from '@/components/MkPollEditor.vue';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue';
import { erase, unique } from '@/scripts/array.js'; import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js'; import { extractMentions } from '@/scripts/extract-mentions.js';
import { formatTimeString } from '@/scripts/format-time-string.js'; import { formatTimeString } from '@/scripts/format-time-string.js';
@@ -150,6 +150,7 @@ const props = withDefaults(defineProps<PostFormProps & {
autofocus: true, autofocus: true,
mock: false, mock: false,
initialLocalOnly: undefined, initialLocalOnly: undefined,
deleteInitialNoteAfterPost: false,
}); });
provide('mock', props.mock); provide('mock', props.mock);
@@ -751,7 +752,7 @@ async function post(ev?: MouseEvent) {
if (ev) { if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
if (el) { if (el && defaultStore.state.animation) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
@@ -845,6 +846,12 @@ async function post(ev?: MouseEvent) {
clear(); clear();
} }
nextTick(() => { nextTick(() => {
// 削除して編集の対象ノートを削除
if (props.initialNote && props.deleteInitialNoteAfterPost) {
misskeyApi('notes/delete', {
noteId: props.initialNote.id,
});
}
deleteDraft(); deleteDraft();
emit('posted'); emit('posted');
if (postData.text && postData.text !== '') { if (postData.text && postData.text !== '') {
@@ -896,6 +903,11 @@ async function post(ev?: MouseEvent) {
if (m === 0 && s === 0) { if (m === 0 && s === 0) {
claimAchievement('postedAt0min0sec'); claimAchievement('postedAt0min0sec');
} }
if (props.initialNote && props.deleteInitialNoteAfterPost) {
if (date.getTime() - new Date(props.initialNote.createdAt).getTime() < 1000 * 60 && props.initialNote.userId === $i.id) {
claimAchievement('noteDeletedWithin1min');
}
}
}); });
}).catch(err => { }).catch(err => {
posting.value = false; posting.value = false;
@@ -1070,6 +1082,8 @@ defineExpose({
&.modal { &.modal {
width: 100%; width: 100%;
max-width: 520px; max-width: 520px;
overflow-x: clip;
overflow-y: auto;
} }
} }

View File

@@ -4,8 +4,23 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()"> <MkModal
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/> ref="modal"
:preferType="'dialog'"
@click="modal?.close()"
@closed="onModalClosed()"
@esc="modal?.close()"
>
<MkPostForm
ref="form"
:class="$style.form"
v-bind="props"
autofocus
freezeAfterPosted
@posted="onPosted"
@cancel="modal?.close()"
@esc="modal?.close()"
/>
</MkModal> </MkModal>
</template> </template>

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<span> <span>
<span>@{{ user.username }}</span> <span>@{{ user.username }}</span>
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> <span v-if="user.host || detail" style="opacity: 0.5;">@{{ user.host || host }}</span>
</span> </span>
</template> </template>
@@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { toUnicode } from 'punycode.js'; import { toUnicode } from 'punycode.js';
import { host as hostRaw } from '@@/js/config.js'; import { host as hostRaw } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
defineProps<{ defineProps<{
user: Misskey.entities.UserLite; user: Misskey.entities.UserLite;

View File

@@ -4,12 +4,14 @@
*/ */
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { defaultStore } from '@/store.js';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
export default { export default {
mounted(el, binding, vn) { mounted(el, binding, vn) {
// 明示的に false であればバインドしない // 明示的に false であればバインドしない
if (binding.value === false) return; if (binding.value === false) return;
if (!defaultStore.state.animation) return;
el.addEventListener('click', () => { el.addEventListener('click', () => {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();

View File

@@ -36,8 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`"> <MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/> <MkUserCardMini :user="file.user"/>
</MkA> </MkA>
<div> <div>
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch> <MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div> </div>
<div> <div>
@@ -117,9 +118,21 @@ async function del() {
}); });
} }
async function toggleIsSensitive(v) { async function toggleSensitive() {
await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v }); if (!file.value) return;
isSensitive.value = v;
const { canceled } = await os.confirm({
type: 'warning',
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
isSensitive.value = !isSensitive.value;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
});
} }
const headerActions = computed(() => [{ const headerActions = computed(() => [{

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo> <MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
@@ -37,21 +37,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template> <template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
</MkKeyValue> </MkKeyValue>
--> -->
<MkKeyValue oneline> <template v-if="!isSystem">
<template #key>{{ i18n.ts.createdAt }}</template> <MkKeyValue oneline>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template> <template #key>{{ i18n.ts.createdAt }}</template>
</MkKeyValue> <template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
<MkKeyValue v-if="info" oneline> </MkKeyValue>
<template #key>{{ i18n.ts.lastActiveDate }}</template> <MkKeyValue v-if="info" oneline>
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template> <template #key>{{ i18n.ts.lastActiveDate }}</template>
</MkKeyValue> <template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
<MkKeyValue v-if="info" oneline> </MkKeyValue>
<template #key>{{ i18n.ts.email }}</template> <MkKeyValue v-if="info" oneline>
<template #value><span class="_monospace">{{ info.email }}</span></template> <template #key>{{ i18n.ts.email }}</template>
</MkKeyValue> <template #value><span class="_monospace">{{ info.email }}</span></template>
</MkKeyValue>
</template>
</div> </div>
<MkTextarea v-model="moderationNote" manualSave> <MkTextarea v-if="!isSystem" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template> <template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template> <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea> </MkTextarea>
@@ -92,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSection> </FormSection>
--> -->
<FormSection> <FormSection v-if="!isSystem">
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
@@ -252,6 +254,7 @@ const ap = ref<any>(null);
const moderator = ref(false); const moderator = ref(false);
const silenced = ref(false); const silenced = ref(false);
const suspended = ref(false); const suspended = ref(false);
const isSystem = ref(false);
const moderationNote = ref(''); const moderationNote = ref('');
const filesPagination = { const filesPagination = {
endpoint: 'admin/drive/files' as const, endpoint: 'admin/drive/files' as const,
@@ -288,6 +291,7 @@ function createFetcher() {
silenced.value = info.value.isSilenced; silenced.value = info.value.isSilenced;
suspended.value = info.value.isSuspended; suspended.value = info.value.isSuspended;
moderationNote.value = info.value.moderationNote; moderationNote.value = info.value.moderationNote;
isSystem.value = user.value.host == null && user.value.username.includes('.');
watch(moderationNote, async () => { watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value }); await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
@@ -507,7 +511,15 @@ watch(user, () => {
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => [{ const headerTabs = computed(() => isSystem.value ? [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : [{
key: 'overview', key: 'overview',
title: i18n.ts.overview, title: i18n.ts.overview,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',

View File

@@ -170,6 +170,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div> </div>
</template> </template>
<template v-else-if="log.type === 'updateProxyAccountDescription'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<details> <details>
<summary>raw</summary> <summary>raw</summary>

View File

@@ -238,15 +238,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-ghost"></i></template> <template #icon><i class="ti ti-ghost"></i></template>
<template #label>{{ i18n.ts.proxyAccount }}</template> <template #label>{{ i18n.ts.proxyAccount }}</template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
<div class="_gaps"> <div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton> <MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</div> </div>
</MkFolder> </MkFolder>
</div> </div>
@@ -256,7 +258,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, reactive } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@@ -277,7 +279,7 @@ import MkRadios from '@/components/MkRadios.vue';
const meta = await misskeyApi('admin/meta'); const meta = await misskeyApi('admin/meta');
const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null); const proxyAccount = await misskeyApi('users/show', { userId: meta.proxyAccountId });
const infoForm = useForm({ const infoForm = useForm({
name: meta.name ?? '', name: meta.name ?? '',
@@ -378,16 +380,14 @@ const federationForm = useForm({
fetchInstance(true); fetchInstance(true);
}); });
function chooseProxyAccount() { const proxyAccountForm = useForm({
os.selectUser({ localOnly: true }).then(user => { description: proxyAccount.description,
proxyAccount.value = user; }, async (state) => {
os.apiWithDialog('admin/update-meta', { await os.apiWithDialog('admin/update-proxy-account', {
proxyAccountId: user.id, description: state.description,
}).then(() => {
fetchInstance(true);
});
}); });
} fetchInstance(true);
});
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@@ -120,6 +120,7 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { apLookup } from '@/scripts/lookup.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -260,13 +261,7 @@ async function search() {
text: i18n.ts.lookupConfirm, text: i18n.ts.lookupConfirm,
}); });
if (!confirm.canceled) { if (!confirm.canceled) {
const promise = misskeyApi('ap/show', { const res = await apLookup(searchParams.value.query);
uri: searchParams.value.query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === 'User') { if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`); router.push(`/@${res.object.username}@${res.object.host}`);

View File

@@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="profile _gaps"> <div class="profile _gaps">
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/> <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!"/>
<MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
<div :key="user.id" class="main _panel"> <div :key="user.id" class="main _panel">
<div class="banner-container" :style="style"> <div class="banner-container" :style="style">
@@ -48,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-if="user.followedMessage != null" class="followedMessage"> <div v-if="user.followedMessage != null" class="followedMessage">
<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow> <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin>
<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div> <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div> <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
</MkFukidashi> </MkFukidashi>

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