Compare commits
27 Commits
2025.2.1
...
renovate/n
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a492f2c7e1 | ||
![]() |
c7cbe60a2d | ||
![]() |
f3be426383 | ||
![]() |
e8a6629cb5 | ||
![]() |
44658ae981 | ||
![]() |
19384efbc5 | ||
![]() |
adf22143aa | ||
![]() |
a17acf647b | ||
![]() |
01a3eabc4e | ||
![]() |
59567a7ccc | ||
![]() |
7fb8fccd57 | ||
![]() |
a4711ab4c1 | ||
![]() |
bbe404a0b2 | ||
![]() |
0610bd657f | ||
![]() |
77667cf80d | ||
![]() |
801a2ec1db | ||
![]() |
2a96e39bb3 | ||
![]() |
616cccf251 | ||
![]() |
7114523d84 | ||
![]() |
5d683728f3 | ||
![]() |
b8632f389d | ||
![]() |
830da5e9f1 | ||
![]() |
e2eddd5b1a | ||
![]() |
d4f9bf1f11 | ||
![]() |
734c78ddd1 | ||
![]() |
c63c3462dd | ||
![]() |
a3bba23b7d |
@@ -5,7 +5,7 @@
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22.11.0"
|
||||
"version": "22.14.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/corepack:1": {
|
||||
"version": "0.31.0"
|
||||
|
2
.github/workflows/get-api-diff.yml
vendored
2
.github/workflows/get-api-diff.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
api-json-name: [api-base.json, api-head.json]
|
||||
include:
|
||||
- api-json-name: api-base.json
|
||||
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
2
.github/workflows/on-release-created.yml
vendored
2
.github/workflows/on-release-created.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
4
.github/workflows/test-backend.yml
vendored
4
.github/workflows/test-backend.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
18
.github/workflows/test-federation.yml
vendored
18
.github/workflows/test-federation.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -62,14 +62,30 @@ jobs:
|
||||
bash ./setup.sh
|
||||
sudo chmod 644 ./certificates/*.test.key
|
||||
- name: Start servers
|
||||
id: start_servers
|
||||
continue-on-error: true
|
||||
# https://github.com/docker/compose/issues/1294#issuecomment-374847206
|
||||
run: |
|
||||
cd packages/backend/test-federation
|
||||
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
|
||||
id: test
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd packages/backend/test-federation
|
||||
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
|
||||
run: |
|
||||
cd packages/backend/test-federation
|
||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
browser: [chrome]
|
||||
|
||||
services:
|
||||
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
2
.github/workflows/validate-api-json.yml
vendored
2
.github/workflows/validate-api-json.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.11.0]
|
||||
node-version: [22.14.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
@@ -1 +1 @@
|
||||
22.11.0
|
||||
22.14.0
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,3 +1,20 @@
|
||||
## 2025.3.0
|
||||
|
||||
### General
|
||||
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
|
||||
- Fix: システムアカウントが削除できる問題を修正
|
||||
|
||||
### Client
|
||||
- Enhance: モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように
|
||||
- Enhance: 「UIのアニメーションを減らす」で画面上のエフェクトも減らせるように
|
||||
- Fix: 削除して編集の削除タイミングを投稿後になるように `#14498`
|
||||
- Fix: フォローされたときのメッセージがちらつくことがある問題を修正
|
||||
- Fix: 投稿ダイアログがサイズ限界を超えた際にスクロールできない問題を修正
|
||||
|
||||
### Server
|
||||
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
|
||||
|
||||
|
||||
## 2025.2.1
|
||||
|
||||
### General
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=22.11.0-bookworm
|
||||
ARG NODE_VERSION=22.14.0-bookworm
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
@@ -233,7 +233,7 @@ describe('After user setup', () => {
|
||||
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
|
||||
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', () => {
|
||||
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
@@ -5262,6 +5262,14 @@ export interface Locale extends ILocale {
|
||||
* " {emoji} " をリアクションしますか?
|
||||
*/
|
||||
"reactAreYouSure": ParameterizedString<"emoji">;
|
||||
/**
|
||||
* このメディアをセンシティブとして設定しますか?
|
||||
*/
|
||||
"markAsSensitiveConfirm": string;
|
||||
/**
|
||||
* このメディアのセンシティブ指定を解除しますか?
|
||||
*/
|
||||
"unmarkAsSensitiveConfirm": string;
|
||||
"_accountSettings": {
|
||||
/**
|
||||
* コンテンツの表示にログインを必須にする
|
||||
@@ -10058,6 +10066,10 @@ export interface Locale extends ILocale {
|
||||
* ギャラリーの投稿を削除
|
||||
*/
|
||||
"deleteGalleryPost": string;
|
||||
/**
|
||||
* プロキシアカウントの説明を更新
|
||||
*/
|
||||
"updateProxyAccountDescription": string;
|
||||
};
|
||||
"_fileViewer": {
|
||||
/**
|
||||
|
@@ -1311,6 +1311,8 @@ federationSpecified: "このサーバーはホワイトリスト連合で運用
|
||||
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
|
||||
confirmOnReact: "リアクションする際に確認する"
|
||||
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
|
||||
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
|
||||
unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
|
||||
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||
@@ -2664,6 +2666,7 @@ _moderationLogTypes:
|
||||
deletePage: "ページを削除"
|
||||
deleteFlash: "Playを削除"
|
||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
||||
|
||||
_fileViewer:
|
||||
title: "ファイルの詳細"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.2.1",
|
||||
"version": "2025.3.0-beta.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"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",
|
||||
"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",
|
||||
"migrate": "cd packages/backend && pnpm migrate",
|
||||
"revert": "cd packages/backend && pnpm revert",
|
||||
@@ -37,7 +37,7 @@
|
||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "pnpm cypress 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-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||
"test": "pnpm -r test",
|
||||
|
37
packages/backend/migration/1740121393164-system-accounts.js
Normal file
37
packages/backend/migration/1740121393164-system-accounts.js
Normal 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"`);
|
||||
}
|
||||
}
|
@@ -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`);
|
||||
}
|
||||
}
|
@@ -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"`);
|
||||
}
|
||||
}
|
@@ -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`);
|
||||
}
|
||||
}
|
@@ -133,7 +133,7 @@ const $meta: Provider = {
|
||||
for (const key in body.after) {
|
||||
(meta as any)[key] = (body.after as any)[key];
|
||||
}
|
||||
meta.proxyAccount = null; // joinなカラムは通常取ってこないので
|
||||
meta.rootUser = null; // joinなカラムは通常取ってこないので
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@@ -10,9 +10,9 @@ import { bindThis } from '@/decorators.js';
|
||||
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -27,7 +27,7 @@ export class AbuseReportService {
|
||||
private idService: IdService,
|
||||
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||
private queueService: QueueService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private apRendererService: ApRendererService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
@@ -136,7 +136,7 @@ export class AbuseReportService {
|
||||
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 flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||
|
@@ -20,10 +20,10 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
@@ -55,12 +55,12 @@ export class AccountMoveService {
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private instanceChart: InstanceChart,
|
||||
private relayService: RelayService,
|
||||
private queueService: QueueService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -126,11 +126,11 @@ export class AccountMoveService {
|
||||
}
|
||||
|
||||
// follow the new account
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
const proxy = await this.systemAccountService.fetch('proxy');
|
||||
const followings = await this.followingsRepository.findBy({
|
||||
followeeId: src.id,
|
||||
followerHost: IsNull(), // follower is local
|
||||
followerId: proxy ? Not(proxy.id) : undefined,
|
||||
followerId: Not(proxy.id),
|
||||
});
|
||||
const followJobs = followings.map(following => ({
|
||||
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
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
if (proxy) {
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
|
||||
}
|
||||
const proxy = await this.systemAccountService.fetch('proxy');
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,6 @@ import { AppLockService } from './AppLockService.js';
|
||||
import { AchievementService } from './AchievementService.js';
|
||||
import { AvatarDecorationService } from './AvatarDecorationService.js';
|
||||
import { CaptchaService } from './CaptchaService.js';
|
||||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||
import { CustomEmojiService } from './CustomEmojiService.js';
|
||||
import { DeleteAccountService } from './DeleteAccountService.js';
|
||||
import { DownloadService } from './DownloadService.js';
|
||||
@@ -37,7 +36,7 @@ import { HashtagService } from './HashtagService.js';
|
||||
import { HttpRequestService } from './HttpRequestService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
import { ImageProcessingService } from './ImageProcessingService.js';
|
||||
import { InstanceActorService } from './InstanceActorService.js';
|
||||
import { SystemAccountService } from './SystemAccountService.js';
|
||||
import { InternalStorageService } from './InternalStorageService.js';
|
||||
import { MetaService } from './MetaService.js';
|
||||
import { MfmService } from './MfmService.js';
|
||||
@@ -69,7 +68,6 @@ import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
import { UserWebhookService } from './UserWebhookService.js';
|
||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import { FileInfoService } from './FileInfoService.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 $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
|
||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
||||
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
|
||||
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 $IdService: Provider = { provide: 'IdService', useExisting: IdService };
|
||||
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
|
||||
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
|
||||
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
|
||||
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
|
||||
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 $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
|
||||
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 $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
|
||||
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
|
||||
@@ -318,7 +314,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
DownloadService,
|
||||
@@ -331,7 +326,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
HttpRequestService,
|
||||
IdService,
|
||||
ImageProcessingService,
|
||||
InstanceActorService,
|
||||
InternalStorageService,
|
||||
MetaService,
|
||||
MfmService,
|
||||
@@ -342,7 +336,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
ProxyAccountService,
|
||||
SystemAccountService,
|
||||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
@@ -465,7 +459,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
$DownloadService,
|
||||
@@ -478,7 +471,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$HttpRequestService,
|
||||
$IdService,
|
||||
$ImageProcessingService,
|
||||
$InstanceActorService,
|
||||
$InternalStorageService,
|
||||
$MetaService,
|
||||
$MfmService,
|
||||
@@ -489,7 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$ProxyAccountService,
|
||||
$SystemAccountService,
|
||||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
@@ -613,7 +605,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
DownloadService,
|
||||
@@ -626,7 +617,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
HttpRequestService,
|
||||
IdService,
|
||||
ImageProcessingService,
|
||||
InstanceActorService,
|
||||
InternalStorageService,
|
||||
MetaService,
|
||||
MfmService,
|
||||
@@ -637,7 +627,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
ProxyAccountService,
|
||||
SystemAccountService,
|
||||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
@@ -759,7 +749,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
$DownloadService,
|
||||
@@ -772,7 +761,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$HttpRequestService,
|
||||
$IdService,
|
||||
$ImageProcessingService,
|
||||
$InstanceActorService,
|
||||
$InternalStorageService,
|
||||
$MetaService,
|
||||
$MfmService,
|
||||
@@ -783,7 +771,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$ProxyAccountService,
|
||||
$SystemAccountService,
|
||||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -13,10 +13,14 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -28,6 +32,7 @@ export class DeleteAccountService {
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -36,8 +41,13 @@ export class DeleteAccountService {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}, 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 });
|
||||
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) {
|
||||
this.moderationLogService.log(moderator, 'deleteAccount', {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -53,7 +53,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||
case 'metaUpdated': {
|
||||
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...(body.after),
|
||||
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||
rootUser: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -113,17 +113,20 @@ export class MetaService implements OnApplicationShutdown {
|
||||
|
||||
if (before) {
|
||||
await transactionalEntityManager.update(MiMeta, before.id, data);
|
||||
|
||||
const metas = await transactionalEntityManager.find(MiMeta, {
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
return metas[0];
|
||||
} 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) {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -4,53 +4,34 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import type { RelaysRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { RelaysRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { MiRelay } from '@/models/Relay.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
const ACTOR_USERNAME = 'relay.actor' as const;
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
|
||||
@Injectable()
|
||||
export class RelayService {
|
||||
private relaysCache: MemorySingleCache<MiRelay[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.relaysRepository)
|
||||
private relaysRepository: RelaysRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private apRendererService: ApRendererService,
|
||||
) {
|
||||
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
|
||||
public async addRelay(inbox: string): Promise<MiRelay> {
|
||||
const relay = await this.relaysRepository.insertOne({
|
||||
@@ -59,8 +40,8 @@ export class RelayService {
|
||||
status: 'requesting',
|
||||
});
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const relayActor = await this.systemAccountService.fetch('relay');
|
||||
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const activity = this.apRendererService.addContext(follow);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||
|
||||
@@ -77,7 +58,7 @@ export class RelayService {
|
||||
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 undo = this.apRendererService.renderUndo(follow, relayActor);
|
||||
const activity = this.apRendererService.addContext(undo);
|
||||
|
@@ -101,7 +101,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
|
||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||
private notificationService: NotificationService;
|
||||
@@ -137,7 +136,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private moderationLogService: ModerationLogService,
|
||||
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.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||
|
||||
@@ -406,15 +404,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
|
||||
@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;
|
||||
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
|
||||
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;
|
||||
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
|
||||
@@ -463,16 +461,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
.map(a => a.userId),
|
||||
);
|
||||
|
||||
if (includeRoot) {
|
||||
const rootUserId = await this.rootUserIdCache.fetch(async () => {
|
||||
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);
|
||||
if (includeRoot && this.meta.rootUserId) {
|
||||
resultSet.add(this.meta.rootUserId);
|
||||
}
|
||||
|
||||
return [...resultSet].sort((x, y) => x.localeCompare(y));
|
||||
|
@@ -14,13 +14,14 @@ 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 generateUserToken from '@/misc/generate-native-user-token.js';
|
||||
import { generateNativeUserToken } from '@/misc/token.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import UsersChart from '@/core/chart/charts/users.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
@@ -41,7 +42,8 @@ export class SignupService {
|
||||
private userService: UserService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private metaService: MetaService,
|
||||
private usersChart: UsersChart,
|
||||
) {
|
||||
}
|
||||
@@ -74,7 +76,7 @@ export class SignupService {
|
||||
}
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
const secret = generateNativeUserToken();
|
||||
|
||||
// Check username duplication
|
||||
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||
@@ -86,9 +88,7 @@ export class SignupService {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
|
||||
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||
|
||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||
if (!opts.ignorePreservedUsernames && this.meta.rootUserId != null) {
|
||||
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||
if (isPreserved) {
|
||||
throw new Error('USED_USERNAME');
|
||||
@@ -129,7 +129,6 @@ export class SignupService {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: this.utilityService.toPunyNullable(host),
|
||||
token: secret,
|
||||
isRoot: isTheFirstUser,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new MiUserKeypair({
|
||||
@@ -153,6 +152,10 @@ export class SignupService {
|
||||
this.usersChart.update(account, true);
|
||||
this.userService.notifySystemWebhook(account, 'userCreated');
|
||||
|
||||
if (this.meta.rootUserId == null) {
|
||||
await this.metaService.update({ rootUserId: account.id });
|
||||
}
|
||||
|
||||
return { account, secret };
|
||||
}
|
||||
}
|
||||
|
172
packages/backend/src/core/SystemAccountService.ts
Normal file
172
packages/backend/src/core/SystemAccountService.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -15,11 +15,11 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||
@@ -43,8 +43,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
private queueService: QueueService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
) {
|
||||
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
@@ -111,10 +111,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (this.userEntityService.isRemoteUser(target)) {
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
if (proxy) {
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
|
||||
}
|
||||
const proxy = await this.systemAccountService.fetch('proxy');
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -73,7 +73,6 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
isLocked: false,
|
||||
isBot: false,
|
||||
isCat: true,
|
||||
isRoot: false,
|
||||
isExplorable: true,
|
||||
isHibernated: false,
|
||||
isDeleted: false,
|
||||
|
@@ -507,19 +507,12 @@ export class ApInboxService {
|
||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
||||
if (user == null) {
|
||||
return 'skip: actor not found';
|
||||
} else if (user.isDeleted) {
|
||||
return 'skip: already deleted';
|
||||
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
|
||||
return 'skip: already deleted or actor not found';
|
||||
}
|
||||
|
||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||
|
||||
await this.usersRepository.update(actor.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
||||
|
||||
return `ok: queued ${job.name} ${job.id}`;
|
||||
|
@@ -23,7 +23,7 @@ import { MfmService } from '@/core/MfmService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.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 { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -39,6 +39,9 @@ export class ApRendererService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -186,7 +189,7 @@ export class ApRendererService {
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
},
|
||||
_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
|
||||
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
|
||||
return {
|
||||
@@ -503,8 +538,8 @@ export class ApRendererService {
|
||||
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
|
||||
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
|
||||
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
|
||||
icon: avatar ? this.renderImage(avatar) : null,
|
||||
image: banner ? this.renderImage(banner) : null,
|
||||
icon: avatar ? this.renderImage(avatar) : isSystem ? this.renderSystemAvatar(user) : this.renderIdenticon(user),
|
||||
image: banner ? this.renderImage(banner) : isSystem ? this.renderSystemBanner() : null,
|
||||
tag,
|
||||
manuallyApprovesFollowers: user.isLocked,
|
||||
discoverable: user.isExplorable,
|
||||
|
@@ -6,7 +6,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
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 { Config } from '@/config.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
@@ -15,13 +14,14 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { LoggerService } from '@/core/LoggerService.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 { ApDbResolverService } from './ApDbResolverService.js';
|
||||
import { ApRendererService } from './ApRendererService.js';
|
||||
import { ApRequestService } from './ApRequestService.js';
|
||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
|
||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||
|
||||
export class Resolver {
|
||||
private history: Set<string>;
|
||||
@@ -37,7 +37,7 @@ export class Resolver {
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -105,7 +105,7 @@ export class Resolver {
|
||||
}
|
||||
|
||||
if (this.config.signToActivityPubGet && !this.user) {
|
||||
this.user = await this.instanceActorService.getInstanceActor();
|
||||
this.user = await this.systemAccountService.fetch('actor');
|
||||
}
|
||||
|
||||
const object = (this.user
|
||||
@@ -119,7 +119,7 @@ export class Resolver {
|
||||
) {
|
||||
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
|
||||
}
|
||||
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ export class ApResolverService {
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -222,7 +222,7 @@ export class ApResolverService {
|
||||
this.noteReactionsRepository,
|
||||
this.followRequestsRepository,
|
||||
this.utilityService,
|
||||
this.instanceActorService,
|
||||
this.systemAccountService,
|
||||
this.apRequestService,
|
||||
this.httpRequestService,
|
||||
this.apRendererService,
|
||||
|
@@ -594,7 +594,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
if (moving) updates.movedAt = new Date();
|
||||
|
||||
// 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) {
|
||||
await this.userPublickeysRepository.update({ userId: exist.id }, {
|
||||
@@ -699,7 +701,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
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 (!user.featured) return;
|
||||
|
||||
|
@@ -11,8 +11,7 @@ import type { MiMeta } from '@/models/Meta.js';
|
||||
import type { AdsRepository } from '@/models/_.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
@@ -29,8 +28,7 @@ export class MetaEntityService {
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
) { }
|
||||
|
||||
@bindThis
|
||||
@@ -149,14 +147,14 @@ export class MetaEntityService {
|
||||
|
||||
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'> = {
|
||||
...packed,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
requireSetup: this.meta.rootUserId == null,
|
||||
proxyAccountName: proxyAccount.username,
|
||||
features: {
|
||||
localTimeline: instance.policies.ltlAvailable,
|
||||
globalTimeline: instance.policies.gtlAvailable,
|
||||
|
@@ -28,6 +28,7 @@ import type {
|
||||
FollowingsRepository,
|
||||
FollowRequestsRepository,
|
||||
MiFollowing,
|
||||
MiMeta,
|
||||
MiUserNotePining,
|
||||
MiUserProfile,
|
||||
MutingsRepository,
|
||||
@@ -100,6 +101,9 @@ export class UserEntityService implements OnModuleInit {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@@ -381,7 +385,11 @@ export class UserEntityService implements OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
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
|
||||
|
@@ -74,6 +74,7 @@ export const DI = {
|
||||
registryItemsRepository: Symbol('registryItemsRepository'),
|
||||
webhooksRepository: Symbol('webhooksRepository'),
|
||||
systemWebhooksRepository: Symbol('systemWebhooksRepository'),
|
||||
systemAccountsRepository: Symbol('systemAccountsRepository'),
|
||||
adsRepository: Symbol('adsRepository'),
|
||||
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
||||
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
||||
|
@@ -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;
|
@@ -5,5 +5,6 @@
|
||||
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default () => secureRndstr(16);
|
||||
export const generateNativeUserToken = () => secureRndstr(16);
|
||||
|
||||
export const isNativeUserToken = (token: string) => token.length === 16;
|
@@ -3,7 +3,7 @@
|
||||
* 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 { MiUser } from './User.js';
|
||||
|
||||
@@ -15,6 +15,18 @@ export class MiMeta {
|
||||
})
|
||||
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', {
|
||||
length: 1024, nullable: true,
|
||||
})
|
||||
@@ -172,18 +184,6 @@ export class MiMeta {
|
||||
})
|
||||
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', {
|
||||
default: false,
|
||||
})
|
||||
|
@@ -3,7 +3,6 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
@@ -63,6 +62,7 @@ import {
|
||||
MiRoleAssignment,
|
||||
MiSignin,
|
||||
MiSwSubscription,
|
||||
MiSystemAccount,
|
||||
MiSystemWebhook,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
@@ -77,8 +77,9 @@ import {
|
||||
MiUserProfile,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook
|
||||
MiWebhook,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
const $usersRepository: Provider = {
|
||||
@@ -285,6 +286,12 @@ const $swSubscriptionsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $systemAccountsRepository: Provider = {
|
||||
provide: DI.systemAccountsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemAccount),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $hashtagsRepository: Provider = {
|
||||
provide: DI.hashtagsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>),
|
||||
@@ -532,6 +539,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$renoteMutingsRepository,
|
||||
$blockingsRepository,
|
||||
$swSubscriptionsRepository,
|
||||
$systemAccountsRepository,
|
||||
$hashtagsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$abuseReportNotificationRecipientRepository,
|
||||
@@ -603,6 +611,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$renoteMutingsRepository,
|
||||
$blockingsRepository,
|
||||
$swSubscriptionsRepository,
|
||||
$systemAccountsRepository,
|
||||
$hashtagsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$abuseReportNotificationRecipientRepository,
|
||||
|
31
packages/backend/src/models/SystemAccount.ts
Normal file
31
packages/backend/src/models/SystemAccount.ts
Normal 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;
|
||||
}
|
@@ -184,12 +184,6 @@ export class MiUser {
|
||||
})
|
||||
public isCat: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the User is the root.',
|
||||
})
|
||||
public isRoot: boolean;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
|
@@ -56,6 +56,7 @@ import { MiRegistryItem } from '@/models/RegistryItem.js';
|
||||
import { MiRelay } from '@/models/Relay.js';
|
||||
import { MiSignin } from '@/models/Signin.js';
|
||||
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserIp } from '@/models/UserIp.js';
|
||||
@@ -171,6 +172,7 @@ export {
|
||||
MiRelay,
|
||||
MiSignin,
|
||||
MiSwSubscription,
|
||||
MiSystemAccount,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
MiUserIp,
|
||||
@@ -242,6 +244,7 @@ export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<
|
||||
export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>;
|
||||
export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
|
||||
export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
|
||||
export type SystemAccountsRepository = Repository<MiSystemAccount> & MiRepository<MiSystemAccount>;
|
||||
export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
|
||||
export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
|
||||
export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
|
||||
|
@@ -82,6 +82,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemAccount } from './models/SystemAccount.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -206,6 +207,7 @@ export const entities = [
|
||||
MiEmoji,
|
||||
MiHashtag,
|
||||
MiSwSubscription,
|
||||
MiSystemAccount,
|
||||
MiAbuseUserReport,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiRegistrationTicket,
|
||||
|
@@ -9,11 +9,11 @@ import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import NotesChart from '@/core/chart/charts/notes.js';
|
||||
import UsersChart from '@/core/chart/charts/users.js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
const nodeinfo2_1path = '/nodeinfo/2.1';
|
||||
@@ -26,7 +26,7 @@ export class NodeinfoServerService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private metaService: MetaService,
|
||||
private notesChart: NotesChart,
|
||||
private usersChart: UsersChart,
|
||||
@@ -70,7 +70,7 @@ export class NodeinfoServerService {
|
||||
const activeHalfyear = 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 };
|
||||
|
||||
@@ -123,7 +123,7 @@ export class NodeinfoServerService {
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
proxyAccountName: proxyAccount.username,
|
||||
themeColor: meta.themeColor ?? '#86b300',
|
||||
},
|
||||
};
|
||||
|
@@ -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);
|
||||
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
|
||||
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 policies = await this.roleService.getUserPolicies(user!.id);
|
||||
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
|
||||
|
@@ -11,7 +11,7 @@ import type { MiAccessToken } from '@/models/AccessToken.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import type { MiApp } from '@/models/App.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';
|
||||
|
||||
export class AuthenticationError extends Error {
|
||||
@@ -46,7 +46,7 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
if (isNativeUserToken(token)) {
|
||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>);
|
||||
|
||||
|
@@ -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/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-proxy-account' from './endpoints/admin/update-proxy-account.js';
|
||||
export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js';
|
||||
export * as 'announcements' from './endpoints/announcements.js';
|
||||
export * as 'announcements/show' from './endpoints/announcements/show.js';
|
||||
|
@@ -4,12 +4,10 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -62,18 +60,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private signupService: SignupService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _me, token) => {
|
||||
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) {
|
||||
// 初期パスワードが設定されている場合
|
||||
@@ -85,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
|
||||
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 外部トークンを使用している場合
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
@@ -42,10 +42,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (user.isRoot) {
|
||||
throw new Error('cannot delete a root account');
|
||||
}
|
||||
|
||||
await this.deleteAccoountService.deleteAccount(user, me);
|
||||
});
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
@@ -237,7 +238,7 @@ export const meta = {
|
||||
},
|
||||
proxyAccountId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
email: {
|
||||
@@ -545,10 +546,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private config: Config,
|
||||
|
||||
private metaService: MetaService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async () => {
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
const proxy = await this.systemAccountService.fetch('proxy');
|
||||
|
||||
return {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
@@ -613,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
|
||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
proxyAccountId: proxy.id,
|
||||
email: instance.email,
|
||||
smtpSecure: instance.smtpSecure,
|
||||
smtpHost: instance.smtpHost,
|
||||
|
@@ -6,7 +6,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
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 { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
@@ -43,6 +43,9 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -58,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
if (user.isRoot) {
|
||||
if (this.serverSettings.rootUserId === user.id) {
|
||||
throw new Error('cannot reset password of root');
|
||||
}
|
||||
|
||||
|
@@ -89,7 +89,6 @@ export const paramDef = {
|
||||
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
||||
setSensitiveFlagAutomatically: { type: 'boolean' },
|
||||
enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
|
||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
maintainerName: { type: 'string', nullable: true },
|
||||
maintainerEmail: { type: 'string', nullable: true },
|
||||
langs: {
|
||||
@@ -394,10 +393,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
|
||||
}
|
||||
|
||||
if (ps.proxyAccountId !== undefined) {
|
||||
set.proxyAccountId = ps.proxyAccountId;
|
||||
}
|
||||
|
||||
if (ps.maintainerName !== undefined) {
|
||||
set.maintainerName = ps.maintainerName;
|
||||
}
|
||||
|
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
|
||||
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 * as Acct from '@/misc/acct.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiMeta } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
@@ -81,6 +83,9 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
@@ -92,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
// check parameter
|
||||
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
|
||||
// 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
|
||||
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import bcrypt from 'bcryptjs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.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 { DI } from '@/di-symbols.js';
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
const newToken = generateUserToken();
|
||||
const newToken = generateNativeUserToken();
|
||||
|
||||
await this.usersRepository.update(me.id, {
|
||||
token: newToken,
|
||||
|
@@ -6,9 +6,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { resetDb } from '@/misc/reset-db.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['non-productive'],
|
||||
@@ -36,13 +39,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private loggerService: LoggerService,
|
||||
private metaService: MetaService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
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);
|
||||
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
|
@@ -122,6 +122,7 @@ export const moderationLogTypes = [
|
||||
'deletePage',
|
||||
'deleteFlash',
|
||||
'deleteGalleryPost',
|
||||
'updateProxyAccountDescription',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
@@ -374,25 +375,29 @@ export type ModerationLogPayloads = {
|
||||
postUserUsername: string;
|
||||
post: any;
|
||||
};
|
||||
updateProxyAccountDescription: {
|
||||
before: string | null;
|
||||
after: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type Serialized<T> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends Date
|
||||
? string
|
||||
: T[K] extends (Date | null)
|
||||
? (string | null)
|
||||
: T[K] extends Record<string, any>
|
||||
? Serialized<T[K]>
|
||||
: T[K] extends (Record<string, any> | null)
|
||||
T[K] extends Date
|
||||
? string
|
||||
: T[K] extends (Date | null)
|
||||
? (string | null)
|
||||
: T[K] extends Record<string, any>
|
||||
? Serialized<T[K]>
|
||||
: T[K] extends (Record<string, any> | null)
|
||||
? (Serialized<T[K]> | null)
|
||||
: T[K] extends (Record<string, any> | undefined)
|
||||
: T[K] extends (Record<string, any> | undefined)
|
||||
? (Serialized<T[K]> | undefined)
|
||||
: T[K];
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type FilterUnionByProperty<
|
||||
Union,
|
||||
Property extends string | number | symbol,
|
||||
Condition
|
||||
Union,
|
||||
Property extends string | number | symbol,
|
||||
Condition,
|
||||
> = Union extends Record<Property, Condition> ? Union : never;
|
||||
|
@@ -12,7 +12,7 @@ services:
|
||||
retries: 20
|
||||
|
||||
misskey:
|
||||
image: node:20
|
||||
image: node:22.14.0
|
||||
env_file:
|
||||
- ./.config/docker.env
|
||||
environment:
|
||||
|
@@ -16,12 +16,16 @@ services:
|
||||
"
|
||||
|
||||
tester:
|
||||
image: node:20
|
||||
image: node:22.14.0
|
||||
depends_on:
|
||||
a.test:
|
||||
condition: service_healthy
|
||||
misskey.a.test:
|
||||
condition: service_healthy
|
||||
b.test:
|
||||
condition: service_healthy
|
||||
misskey.b.test:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
|
||||
@@ -82,7 +86,7 @@ services:
|
||||
command: pnpm -F backend test:fed
|
||||
|
||||
daemon:
|
||||
image: node:20
|
||||
image: node:22.14.0
|
||||
depends_on:
|
||||
redis.test:
|
||||
condition: service_healthy
|
||||
|
@@ -35,7 +35,7 @@ describe('Abuse report', () => {
|
||||
const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
|
||||
const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
|
||||
// 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);
|
||||
|
||||
// NOTE: cannot forward multiple times
|
||||
|
@@ -37,6 +37,7 @@ describe('User', () => {
|
||||
'id',
|
||||
'host',
|
||||
'avatarUrl',
|
||||
'avatarBlurhash',
|
||||
'instance',
|
||||
'badgeRoles',
|
||||
'url',
|
||||
@@ -379,7 +380,8 @@ describe('User', () => {
|
||||
strictEqual(followers.length, 1); // followed by Bob
|
||||
|
||||
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 });
|
||||
strictEqual(following.length, 0); // no following relation
|
||||
@@ -477,7 +479,8 @@ describe('User', () => {
|
||||
strictEqual(followers.length, 1); // followed by Bob
|
||||
|
||||
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 });
|
||||
strictEqual(following.length, 0); // no following relation
|
||||
|
@@ -36,7 +36,7 @@ export type Request = <
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@@ -7,14 +7,10 @@ import type { Config } from '@/config.js';
|
||||
import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import type { ApRendererService } from '@/core/activitypub/ApRendererService.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 { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { MetaService } from '@/core/MetaService.js';
|
||||
import type { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type {
|
||||
FollowRequestsRepository,
|
||||
MiMeta,
|
||||
@@ -23,6 +19,9 @@ import type {
|
||||
PollsRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Resolver } from '@/core/activitypub/ApResolverService.js';
|
||||
|
||||
type MockResponse = {
|
||||
type: string;
|
||||
@@ -43,7 +42,7 @@ export class MockResolver extends Resolver {
|
||||
{} as NoteReactionsRepository,
|
||||
{} as FollowRequestsRepository,
|
||||
{} as UtilityService,
|
||||
{} as InstanceActorService,
|
||||
{} as SystemAccountService,
|
||||
{} as ApRequestService,
|
||||
{} as HttpRequestService,
|
||||
{} as ApRendererService,
|
||||
|
@@ -149,9 +149,9 @@ describe('AbuseReportNotificationService', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
|
||||
root = await createUser({ username: 'root', usernameLower: 'root' });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice' });
|
||||
bob = await createUser({ username: 'bob', usernameLower: 'bob' });
|
||||
systemWebhook1 = await createWebhook();
|
||||
systemWebhook2 = await createWebhook();
|
||||
|
||||
|
@@ -79,9 +79,9 @@ describe('FlashService', () => {
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
idService = app.get(IdService);
|
||||
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
|
||||
root = await createUser({ username: 'root', usernameLower: 'root' });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice' });
|
||||
bob = await createUser({ username: 'bob', usernameLower: 'bob' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@@ -3,24 +3,21 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { ModuleMocker } from 'jest-mock';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
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 { ModuleMocker } from 'jest-mock';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
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);
|
||||
|
||||
@@ -28,8 +25,6 @@ describe('RelayService', () => {
|
||||
let app: TestingModule;
|
||||
let relayService: RelayService;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let relaysRepository: RelaysRepository;
|
||||
let userEntityService: UserEntityService;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
@@ -38,10 +33,10 @@ describe('RelayService', () => {
|
||||
],
|
||||
providers: [
|
||||
IdService,
|
||||
CreateSystemUserService,
|
||||
ApRendererService,
|
||||
RelayService,
|
||||
UserEntityService,
|
||||
SystemAccountService,
|
||||
UtilityService,
|
||||
],
|
||||
})
|
||||
@@ -61,8 +56,6 @@ describe('RelayService', () => {
|
||||
|
||||
relayService = app.get<RelayService>(RelayService);
|
||||
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
||||
relaysRepository = app.get<RelaysRepository>(DI.relaysRepository);
|
||||
userEntityService = app.get<UserEntityService>(UserEntityService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@@ -57,6 +57,12 @@ describe('RoleService', () => {
|
||||
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> = {}) {
|
||||
const x = await rolesRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
@@ -279,7 +285,7 @@ describe('RoleService', () => {
|
||||
describe('getModeratorIds', () => {
|
||||
test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
|
||||
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 });
|
||||
@@ -305,7 +311,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
|
||||
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 });
|
||||
@@ -331,7 +337,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
|
||||
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 });
|
||||
@@ -357,7 +363,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
|
||||
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 });
|
||||
@@ -383,7 +389,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
|
||||
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 });
|
||||
@@ -409,7 +415,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('root has moderator role', async () => {
|
||||
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 });
|
||||
@@ -433,7 +439,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('root has administrator role', async () => {
|
||||
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 });
|
||||
@@ -457,7 +463,7 @@ describe('RoleService', () => {
|
||||
|
||||
test('root has moderator role(expire)', async () => {
|
||||
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 });
|
||||
|
@@ -97,7 +97,7 @@ describe('SystemWebhookService', () => {
|
||||
}
|
||||
|
||||
async function beforeEachImpl() {
|
||||
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||
root = await createUser({ username: 'root', usernameLower: 'root' });
|
||||
}
|
||||
|
||||
async function afterEachImpl() {
|
||||
|
@@ -113,7 +113,7 @@ describe('UserSearchService', () => {
|
||||
});
|
||||
|
||||
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' });
|
||||
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
|
||||
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
|
||||
|
@@ -91,7 +91,7 @@ describe('UserWebhookService', () => {
|
||||
}
|
||||
|
||||
async function beforeEachImpl() {
|
||||
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||
root = await createUser({ username: 'root', usernameLower: 'root' });
|
||||
}
|
||||
|
||||
async function afterEachImpl() {
|
||||
|
@@ -88,8 +88,8 @@ describe('WebhookTestService', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||
root = await createUser({ username: 'root', usernameLower: 'root' });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice' });
|
||||
|
||||
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
|
||||
|
@@ -316,7 +316,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
|
||||
createUser({}, { email: 'user2@example.com', emailVerified: false }),
|
||||
createUser({}, { email: null, emailVerified: false }),
|
||||
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]);
|
||||
@@ -349,7 +349,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
|
||||
createUser({}, { email: 'user2@example.com', emailVerified: false }),
|
||||
createUser({}, { email: null, emailVerified: false }),
|
||||
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]);
|
||||
|
@@ -25,16 +25,16 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"frontend-shared": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.34.8",
|
||||
"sass": "1.85.0",
|
||||
"shiki": "3.0.0",
|
||||
"rollup": "4.34.9",
|
||||
"sass": "1.85.1",
|
||||
"shiki": "3.1.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.8.2",
|
||||
"uuid": "11.1.0",
|
||||
"json5": "2.2.3",
|
||||
"vite": "6.1.1",
|
||||
"vite": "6.2.0",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,29 +42,29 @@
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.6",
|
||||
"@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/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.1",
|
||||
"@typescript-eslint/parser": "8.24.1",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"@types/ws": "8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"@vitest/coverage-v8": "3.0.7",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"eslint-plugin-vue": "9.33.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "17.1.4",
|
||||
"happy-dom": "17.2.2",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.7.1",
|
||||
"msw": "2.7.3",
|
||||
"nodemon": "3.1.9",
|
||||
"prettier": "3.5.2",
|
||||
"prettier": "3.5.3",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"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-tsc": "2.2.4"
|
||||
"vue-tsc": "2.2.8"
|
||||
}
|
||||
}
|
||||
|
@@ -21,13 +21,13 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.13.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.1",
|
||||
"@typescript-eslint/parser": "8.24.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"esbuild": "0.25.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"eslint-plugin-vue": "9.33.0",
|
||||
"nodemon": "3.1.9",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.8.2",
|
||||
"vue-eslint-parser": "9.4.3"
|
||||
},
|
||||
"files": [
|
||||
|
@@ -40,9 +40,9 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "11.25.2",
|
||||
"chromatic": "11.27.0",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0-rc.2",
|
||||
"cropperjs": "2.0.0",
|
||||
"date-fns": "4.1.0",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
@@ -58,84 +58,84 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.34.8",
|
||||
"rollup": "4.34.9",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sass": "1.85.0",
|
||||
"shiki": "3.0.0",
|
||||
"sass": "1.85.1",
|
||||
"shiki": "3.1.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.173.0",
|
||||
"three": "0.174.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsc-alias": "1.8.11",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.8.2",
|
||||
"uuid": "11.1.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.1.1",
|
||||
"vite": "6.2.0",
|
||||
"vue": "3.5.13",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.0",
|
||||
"@storybook/addon-actions": "8.5.8",
|
||||
"@storybook/addon-essentials": "8.5.8",
|
||||
"@storybook/addon-interactions": "8.5.8",
|
||||
"@storybook/addon-links": "8.5.8",
|
||||
"@storybook/addon-mdx-gfm": "8.5.8",
|
||||
"@storybook/addon-storysource": "8.5.8",
|
||||
"@storybook/blocks": "8.5.8",
|
||||
"@storybook/components": "8.5.8",
|
||||
"@storybook/core-events": "8.5.8",
|
||||
"@storybook/manager-api": "8.5.8",
|
||||
"@storybook/preview-api": "8.5.8",
|
||||
"@storybook/react": "8.5.8",
|
||||
"@storybook/react-vite": "8.5.8",
|
||||
"@storybook/test": "8.5.8",
|
||||
"@storybook/theming": "8.5.8",
|
||||
"@storybook/types": "8.5.8",
|
||||
"@storybook/vue3": "8.5.8",
|
||||
"@storybook/vue3-vite": "8.5.8",
|
||||
"@storybook/addon-actions": "8.6.3",
|
||||
"@storybook/addon-essentials": "8.6.3",
|
||||
"@storybook/addon-interactions": "8.6.3",
|
||||
"@storybook/addon-links": "8.6.3",
|
||||
"@storybook/addon-mdx-gfm": "8.6.3",
|
||||
"@storybook/addon-storysource": "8.6.3",
|
||||
"@storybook/blocks": "8.6.3",
|
||||
"@storybook/components": "8.6.3",
|
||||
"@storybook/core-events": "8.6.3",
|
||||
"@storybook/manager-api": "8.6.3",
|
||||
"@storybook/preview-api": "8.6.3",
|
||||
"@storybook/react": "8.6.3",
|
||||
"@storybook/react-vite": "8.6.3",
|
||||
"@storybook/test": "8.6.3",
|
||||
"@storybook/theming": "8.6.3",
|
||||
"@storybook/types": "8.6.3",
|
||||
"@storybook/vue3": "8.6.3",
|
||||
"@storybook/vue3-vite": "8.6.3",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.6",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@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/sanitize-html": "2.13.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.1",
|
||||
"@typescript-eslint/parser": "8.24.1",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"@types/ws": "8.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"@vitest/coverage-v8": "3.0.7",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "14.0.3",
|
||||
"cypress": "14.1.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"eslint-plugin-vue": "9.33.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "17.1.4",
|
||||
"happy-dom": "17.2.2",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.7.1",
|
||||
"msw": "2.7.3",
|
||||
"msw-storybook-addon": "2.0.4",
|
||||
"nodemon": "3.1.9",
|
||||
"prettier": "3.5.2",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"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",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.0.6",
|
||||
"vitest-fetch-mock": "0.4.3",
|
||||
"vue-component-type-helpers": "2.2.4",
|
||||
"vitest": "3.0.7",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "2.2.8",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.2.4"
|
||||
"vue-tsc": "2.2.8"
|
||||
}
|
||||
}
|
||||
|
@@ -413,7 +413,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
||||
|
||||
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
if (el && defaultStore.state.animation) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
|
@@ -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', {
|
||||
fileId: file.id,
|
||||
isSensitive: !file.isSensitive,
|
||||
|
@@ -124,11 +124,21 @@ function showMenu(ev: MouseEvent) {
|
||||
|
||||
if (iAmModerator) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.markAsSensitive,
|
||||
text: props.image.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
action: async () => {
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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', {
|
||||
fileId: file.id,
|
||||
isSensitive: !file.isSensitive,
|
||||
|
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<img :class="$style.icon" :src="avatarUrl" alt="">
|
||||
<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>
|
||||
</MkA>
|
||||
</template>
|
||||
@@ -17,10 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { toUnicode } from 'punycode.js';
|
||||
import { computed } from 'vue';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
import type { MkABehavior } from '@/components/global/MkA.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import type { MkABehavior } from '@/components/global/MkA.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
|
@@ -479,7 +479,7 @@ function react(): void {
|
||||
reaction: '❤️',
|
||||
});
|
||||
const el = reactButton.value;
|
||||
if (el) {
|
||||
if (el && defaultStore.state.animation) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
|
@@ -442,7 +442,7 @@ function react(): void {
|
||||
reaction: '❤️',
|
||||
});
|
||||
const el = reactButton.value;
|
||||
if (el) {
|
||||
if (el && defaultStore.state.animation) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
|
@@ -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>
|
||||
<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">
|
||||
<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 :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
||||
<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>
|
||||
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 Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { toASCII } from 'punycode.js';
|
||||
import { host, url } from '@@/js/config.js';
|
||||
import type { ShallowRef } from 'vue';
|
||||
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 XPostFormAttaches from '@/components/MkPostFormAttaches.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 { extractMentions } from '@/scripts/extract-mentions.js';
|
||||
import { formatTimeString } from '@/scripts/format-time-string.js';
|
||||
@@ -150,6 +150,7 @@ const props = withDefaults(defineProps<PostFormProps & {
|
||||
autofocus: true,
|
||||
mock: false,
|
||||
initialLocalOnly: undefined,
|
||||
deleteInitialNoteAfterPost: false,
|
||||
});
|
||||
|
||||
provide('mock', props.mock);
|
||||
@@ -751,7 +752,7 @@ async function post(ev?: MouseEvent) {
|
||||
if (ev) {
|
||||
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
||||
|
||||
if (el) {
|
||||
if (el && defaultStore.state.animation) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
@@ -845,6 +846,12 @@ async function post(ev?: MouseEvent) {
|
||||
clear();
|
||||
}
|
||||
nextTick(() => {
|
||||
// 削除して編集の対象ノートを削除
|
||||
if (props.initialNote && props.deleteInitialNoteAfterPost) {
|
||||
misskeyApi('notes/delete', {
|
||||
noteId: props.initialNote.id,
|
||||
});
|
||||
}
|
||||
deleteDraft();
|
||||
emit('posted');
|
||||
if (postData.text && postData.text !== '') {
|
||||
@@ -896,6 +903,11 @@ async function post(ev?: MouseEvent) {
|
||||
if (m === 0 && s === 0) {
|
||||
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 => {
|
||||
posting.value = false;
|
||||
@@ -1070,6 +1082,8 @@ defineExpose({
|
||||
&.modal {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
overflow-x: clip;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,8 +4,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal 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
|
||||
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>
|
||||
</template>
|
||||
|
||||
|
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { toUnicode } from 'punycode.js';
|
||||
import { host as hostRaw } from '@@/js/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
defineProps<{
|
||||
user: Misskey.entities.UserLite;
|
||||
|
@@ -4,12 +4,14 @@
|
||||
*/
|
||||
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vn) {
|
||||
// 明示的に false であればバインドしない
|
||||
if (binding.value === false) return;
|
||||
if (!defaultStore.state.animation) return;
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
@@ -36,8 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
|
||||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
|
||||
<div>
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
|
||||
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -117,9 +118,21 @@ async function del() {
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleIsSensitive(v) {
|
||||
await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v });
|
||||
isSensitive.value = v;
|
||||
async function toggleSensitive() {
|
||||
if (!file.value) return;
|
||||
|
||||
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(() => [{
|
||||
|
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</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>
|
||||
|
||||
@@ -37,21 +37,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
|
||||
</MkKeyValue>
|
||||
-->
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.lastActiveDate }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value><span class="_monospace">{{ info.email }}</span></template>
|
||||
</MkKeyValue>
|
||||
<template v-if="!isSystem">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.lastActiveDate }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value><span class="_monospace">{{ info.email }}</span></template>
|
||||
</MkKeyValue>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<MkTextarea v-if="!isSystem" v-model="moderationNote" manualSave>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
|
||||
</MkTextarea>
|
||||
@@ -92,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</FormSection>
|
||||
-->
|
||||
|
||||
<FormSection>
|
||||
<FormSection v-if="!isSystem">
|
||||
<div class="_gaps">
|
||||
<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 silenced = ref(false);
|
||||
const suspended = ref(false);
|
||||
const isSystem = ref(false);
|
||||
const moderationNote = ref('');
|
||||
const filesPagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
@@ -288,6 +291,7 @@ function createFetcher() {
|
||||
silenced.value = info.value.isSilenced;
|
||||
suspended.value = info.value.isSuspended;
|
||||
moderationNote.value = info.value.moderationNote;
|
||||
isSystem.value = user.value.host == null && user.value.username.includes('.');
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
|
||||
@@ -507,7 +511,15 @@ watch(user, () => {
|
||||
|
||||
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',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
@@ -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"/>
|
||||
</div>
|
||||
</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>
|
||||
<summary>raw</summary>
|
||||
|
@@ -238,15 +238,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ghost"></i></template>
|
||||
<template #label>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template v-if="proxyAccountForm.modified.value" #footer>
|
||||
<MkFormFooter :form="proxyAccountForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<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>
|
||||
</MkFolder>
|
||||
</div>
|
||||
@@ -256,7 +258,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
@@ -277,7 +279,7 @@ import MkRadios from '@/components/MkRadios.vue';
|
||||
|
||||
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({
|
||||
name: meta.name ?? '',
|
||||
@@ -378,16 +380,14 @@ const federationForm = useForm({
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
function chooseProxyAccount() {
|
||||
os.selectUser({ localOnly: true }).then(user => {
|
||||
proxyAccount.value = user;
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: user.id,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
const proxyAccountForm = useForm({
|
||||
description: proxyAccount.description,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-proxy-account', {
|
||||
description: state.description,
|
||||
});
|
||||
}
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
|
@@ -120,6 +120,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { apLookup } from '@/scripts/lookup.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
@@ -260,13 +261,7 @@ async function search() {
|
||||
text: i18n.ts.lookupConfirm,
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
const promise = misskeyApi('ap/show', {
|
||||
uri: searchParams.value.query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
const res = await apLookup(searchParams.value.query);
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
|
@@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div class="profile _gaps">
|
||||
<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 class="banner-container" :style="style">
|
||||
@@ -48,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<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><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
|
||||
</MkFukidashi>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user