Compare commits

...

45 commits

Author SHA1 Message Date
009c4effc4
more work towards actor key proxy
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-06 13:35:07 -06:00
6b0cc0d725
Merge upstream 2025.3.0
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-06 11:37:23 -06:00
f0a6270343
fix(backend): fixup migration for incorrect extraction on system accounts table
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-06 11:08:25 -06:00
edee905c07
move rust build environment to use debian too for compatibility
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-06 10:15:59 -06:00
syuilo
0214a0001f
feat(frontend): 設定の検索 ()
* wip

* wip

* wip

* test

* wip rollup pluginでsearchIndexの情報生成

* wip

* SPDX

* wip: markerIdを自動付与

* rollupでビルド時・devモード時に毎回uuidを生成するように

* 開発サーバーでだけ必要な挙動は開発サーバーのみで

* 条件が逆

* wip: childrenの生成

* update comment

* update comment

* rename auto generated file

* hashをパスと行数から決定

* Update privacy.vue

* Update privacy.vue

* wip

* Update general.vue

* Update general.vue

* wip

* wip

* Update SearchMarker.vue

* wip

* Update profile.vue

* Update mute-block.vue

* Update mute-block.vue

* Update general.vue

* Update general.vue

* childrenがduplicate key errorを吐く問題をいったん解決

* マーカーの形を成形

* loggerを置きかえ

* とりあえず省略記法に対応

* Refactor and Format codes

* wip

* Update settings-search-index.ts

* wip

* wip

* とりあえず不確定要因の仮置きidを削除

* hashの生成を正規化(絶対パスになっていたのを緩和)

* pathの入力を省略可能に

* adminでもパス生成できるように

* Update settings-search-index.ts

* Update privacy.vue

* wip

* build searchIndex

* wip

* build

* Update general.vue

* build

* Update sounds.vue

* build

* build

* Update sounds.vue

* 🎨

* 🎨

* Update privacy.vue

* Update privacy.vue

* Update security.vue

* create-search-indexを多少改善

* build

* Update 2fa.vue

* wip

* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義

* キャッシュはdevServerでなくても更新

* Revert "wip"

This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054.

* inlining

* wip

* Update theme.vue

* 🎨

* wip normalize

* Update theme.vue

* キャッシュのパス変換

* build

* wip

* wip

* Update SearchMarker.vue

* i18n.ts['key'] の形式が取り出せない問題のFix

* build

* 仮でpath入れ

* 必ず絶対パスが使われるように

* wip

* 🎨

* storybookビルド時はcreateSearchIndexをしない

* inliningの構造化

* format code

* Update index.vue

* wip

* wip

* 🎨

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* clean up

* Update navbar.vue

* enhance: 検索で上下矢印を使用することで検索結果を移動できるように

* refactor

* fix(frontend): PageWindowでSearchMarkerが動作するように

* enhance(frontend): SearchMarkerの点滅を一定時間で止める

* lint fix

* fix: 子要素監視が抜けていたのを修正

* アニメーションの回数はCSSで制御するように

* refactor

* enhance(frontend): 検索インデックス作成時のログを削減

* revert

* fix

* fix

---------

Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-03-06 23:15:19 +09:00
github-actions[bot]
46067f6e17 [skip ci] Update CHANGELOG.md (prepend template) 2025-03-06 10:31:36 +00:00
github-actions[bot]
2b71bdf114 Release: 2025.3.0 2025-03-06 10:31:30 +00:00
github-actions[bot]
9d6b521351 Bump version to 2025.3.0-beta.2 2025-03-06 08:19:56 +00:00
syuilo
ad708d896b
Merge pull request from misskey-dev/l10n_develop
New Crowdin updates
2025-03-06 17:06:09 +09:00
かっこかり
22228b6756
enhance: OAuth2 (IndieAuth) でロゴが提供されている場合は表示するように ()
* enhance: OAuthでロゴが提供されている場合は表示するように

* Update Changelog

* refactor

* fix

* fix test
2025-03-06 08:05:14 +00:00
かっこかり
f7ea0c6991
fix(backend): S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正 ()
* fix(backend/object-storage): disable data integrity protections (MisskeyIO#895)

Cloudflare R2 does not support 'x-amz-checksum-*'

* Update Changelog

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2025-03-06 08:03:57 +00:00
Sayamame-beans
60a3513cfc
enhance(frontend): invert how to show the number of attachments(remains) on postform ()
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-06 07:45:46 +00:00
github-actions[bot]
377f002d68 Bump version to 2025.3.0-beta.1 2025-03-06 07:31:52 +00:00
syuilo
896bde1005 revert https://github.com/misskey-dev/misskey/pull/15545
see https://github.com/misskey-dev/misskey/issues/14498
2025-03-06 16:28:25 +09:00
かっこかり
6d0242277d
fix(frontend): tabler-iconsが読み込めない問題を修正(正式リリースに差し替え) () 2025-03-06 05:34:24 +00:00
renovate[bot]
60f90ca649
chore(deps): update [misskey-js] update dependencies ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 07:09:01 +09:00
renovate[bot]
f3be426383
fix(deps): update [frontend] update dependencies ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 21:01:44 +09:00
かっこかり
e8a6629cb5
fix(backend): システムアカウント系のマイグレーション不足を修正 ()
* fix(backend): プロキシアカウントのロールバック用マイグレーションを追加

* fix

* separate newly-added `up` command

* drop backwards-compatibility

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

This reverts commit a4711ab4c1.

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

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

* fix

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

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

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

* Update Changelog

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

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

* use MkSwitch

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

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

* update CHANGELOG.md

* 指摘対応

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

* 指摘対応のミス修正

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

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-03-03 08:05:18 +00:00
e901b72024
dynamic apparmor stage 2 - actor key proxy
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-02 06:54:57 -06:00
github-actions[bot]
2a96e39bb3 Bump version to 2025.3.0-alpha.0 2025-03-02 12:12:06 +00:00
syuilo
616cccf251
enhance(backend): refine system account ()
* wip

* wip

* wip

* Update SystemAccountService.ts

* Update 1740121393164-system-accounts.js

* Update DeleteAccountService.ts

* wip

* wip

* wip

* wip

* Update 1740121393164-system-accounts.js

* Update RepositoryModule.ts

* wip

* wip

* wip

* Update ApRendererService.ts

* wip

* wip

* Update SystemAccountService.ts

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* add print logs

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

* fix migration

* refactor

* fix fed-tests

* Update RelayService.ts

* merge

* Update user.test.ts

* chore: emit log

* fix: tweak sleep duration

* fix: exit 1

* fix: wait for misskey processes to become healthy

* fix: longer sleep for user deletion

* fix: make sleep longer again

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

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

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

This reverts commit 266141f66fb584371bbb56ef7eba04e14bcff94d.

* wip

* Update SystemAccountService.ts

---------

Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-03-02 20:06:20 +09:00
5f766a1f75
retire setuid CD program
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-01 22:20:26 -06:00
syuilo
7114523d84
Update CHANGELOG.md 2025-03-01 17:02:52 +09:00
syuilo
5d683728f3
デッドロック解消の試み ()
https://github.com/misskey-dev/misskey/issues/15005

Co-authored-by: 饺子w (Yumechi) <35571479+eternal-flame-AD@users.noreply.github.com>
2025-03-01 16:12:42 +09:00
おさむのひと
b8632f389d
chore(ci): Renovateが作ったprにラベルつける () 2025-03-01 04:37:11 +00:00
renovate[bot]
830da5e9f1
fix(deps): update [frontend] update dependencies ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:11:09 +09:00
renovate[bot]
e2eddd5b1a
chore(deps): update actions/cache action to v4.2.2 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:59 +09:00
renovate[bot]
d4f9bf1f11
chore(deps): update [misskey-js] update dependencies ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:47 +09:00
renovate[bot]
734c78ddd1
chore(deps): update [tools] update dependencies ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 13:07:36 +09:00
10258ecddc
restore canonical service worker
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-02-28 12:17:32 -06:00
syuilo
c63c3462dd refactor 2025-02-28 09:34:21 +09:00
e60bbcef21
lint
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-02-27 13:14:35 -06:00
e00aad4ae6
fix: fix object normalization for replies
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-02-27 13:08:35 -06:00
179 changed files with 8663 additions and 3993 deletions
.github/workflows
.vscode
CHANGELOG.mdContainerfile
cypress/e2e
locales
package.json
packages
backend
migration
src
test-federation
test
frontend-embed
frontend-shared
frontend

View file

@ -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 }}

View file

@ -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

View file

@ -13,7 +13,6 @@
},
"editor.formatOnSave": false,
"rust-analyzer.linkedProjects": [
"yume-mods/nyuukyou/Cargo.toml",
"yume-mods/misskey-auto-deploy/Cargo.toml",
"yume-mods/Cargo.toml",
]
}

View file

@ -1,4 +1,30 @@
## 2025.3.0-yumechinokuni.0
### Server
- Fix: DBマイグレーション際にシステムアカウントのユーザーID判定が正しくない問題を修正
## 2025.3.0
### General
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。
- Fix: システムアカウントが削除できる問題を修正
### Client
- Enhance: モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように
- Enhance: 「UIのアニメーションを減らす」で画面上のエフェクトも減らせるように
- Enhance: 投稿フォームにおける、メディアの添付可能個数のカウントを反転しました
- これまでの表示は`添付可能残り個数/上限数`でしたが、`添付個数/上限数`としました
- Fix: フォローされたときのメッセージがちらつくことがある問題を修正
- Fix: 投稿ダイアログがサイズ限界を超えた際にスクロールできない問題を修正
### Server
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895)
## 2025.2.1-yumechinokuni.0
- Reflect upstream changes

View file

@ -6,15 +6,19 @@
ARG NODE_VERSION=22.11.0-bookworm
FROM rust:1-alpine AS rust-builder
FROM rust:1-bookworm AS rust-builder
RUN apk add --no-cache build-base libcap-ng-static
RUN apt-get update \
&& apt-get install -yqq --no-install-recommends \
build-essential libcap-ng-dev libapparmor-dev
COPY ./yume-mods /yume-mods
WORKDIR /yume-mods
RUN cargo build -p misskey-auto-deploy-entrypoint --release
ARG ENTRYPOINT_FEATURES=""
RUN cargo build -p misskey-auto-deploy-entrypoint --release --features "${ENTRYPOINT_FEATURES}"
FROM node:${NODE_VERSION} AS builder
@ -50,7 +54,7 @@ ENV COREPACK_DEFAULT_TO_LATEST=0
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg curl libjemalloc-dev libjemalloc2 \
ffmpeg curl libjemalloc-dev libjemalloc2 libcap-ng0 libapparmor1 \
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \
&& corepack enable \
&& mkdir -p /misskey \

View file

@ -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', () => {

View file

@ -260,7 +260,7 @@ noCustomEmojis: "No hi ha emojis personalitzats"
noJobs: "No hi ha feines"
federating: "Federant"
blocked: "Bloquejat"
suspended: "Suspés"
suspended: "Anul·lar subscripció "
all: "tot"
subscribing: "Subscrit a"
publishing: "S'està publicant"
@ -1311,6 +1311,8 @@ federationSpecified: "Aquest servidor treballa amb una federació de llistes bla
federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors."
confirmOnReact: "Confirmar en reaccionar"
reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?"
markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?"
unmarkAsSensitiveConfirm: "Vols deixar de marcar com a sensible aquest contingut?"
_accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
@ -1332,7 +1334,7 @@ _abuseUserReport:
resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament."
_delivery:
status: "Estat d'entrega "
stop: "Suspés"
stop: "Anul·lar subscripció "
resume: "Torna a enviar"
_type:
none: "S'està publicant"
@ -2594,6 +2596,7 @@ _moderationLogTypes:
deletePage: "Esborrar la pàgina"
deleteFlash: "Esborrar el guió"
deleteGalleryPost: "Esborrar la publicació de la galeria"
updateProxyAccountDescription: "Actualitzar descripció del compte proxy"
_fileViewer:
title: "Detall del fitxer"
type: "Tipus de fitxer"

View file

@ -5,6 +5,7 @@ introMisskey: "Vítejte! Misskey je otevřený a decentralizovaný microblogový
poweredByMisskeyDescription: "{name} je jeden ze serverů využívající open source platformu <b>Misskey<b> (nazývaná \"Misskey instance\")."
monthAndDay: "{day}. {month}."
search: "Vyhledávání"
reset: "Obnovit"
notifications: "Oznámení"
username: "Uživatelské jméno"
password: "Heslo"
@ -365,8 +366,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivovat hCaptchu"
hcaptchaSiteKey: "Klíč stránky"
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
mcaptcha: "mCaptcha"
enableMcaptcha: "Aktivovat mCaptchu"
mcaptchaSiteKey: "Klíč stránky"
mcaptchaSecretKey: "Tajný Klíč (Secret Key)"
mcaptchaInstanceUrl: "URL mCaptcha serveru"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Zapnout ReCAPTCHu"
recaptchaSiteKey: "Klíč stránky"

22
locales/index.d.ts vendored
View file

@ -4971,7 +4971,7 @@ export interface Locale extends ILocale {
*/
"disableStreamingTimeline": string;
/**
*
*
*/
"useGroupedNotifications": string;
/**
@ -5262,6 +5262,22 @@ export interface Locale extends ILocale {
* " {emoji} "
*/
"reactAreYouSure": ParameterizedString<"emoji">;
/**
*
*/
"markAsSensitiveConfirm": string;
/**
*
*/
"unmarkAsSensitiveConfirm": string;
/**
*
*/
"preferences": string;
/**
*
*/
"accessibility": string;
"_accountSettings": {
/**
*
@ -10062,6 +10078,10 @@ export interface Locale extends ILocale {
* 稿
*/
"deleteGalleryPost": string;
/**
*
*/
"updateProxyAccountDescription": string;
};
"_fileViewer": {
/**

View file

@ -126,7 +126,7 @@ pinnedNote: "Nota in primo piano"
pinned: "Fissa sul profilo"
you: "Tu"
clickToShow: "Contenuto occultato, cliccare solo se si intende vedere"
sensitive: "Allegato esplicito"
sensitive: "Esplicito"
add: "Aggiungi"
reaction: "Reazioni"
reactions: "Reazioni"
@ -228,7 +228,7 @@ jobQueue: "Coda di lavoro"
cpuAndMemory: "CPU e Memoria"
network: "Rete"
disk: "Disco"
instanceInfo: "Informazioni sull'istanza"
instanceInfo: "Informazioni sul server"
statistics: "Statistiche"
clearQueue: "Svuota coda"
clearQueueConfirmTitle: "Vuoi davvero svuotare la coda?"
@ -445,7 +445,7 @@ exploreFediverse: "Esplora il Fediverso"
popularTags: "Hashtag popolari"
userList: "Liste"
about: "Informazioni"
aboutMisskey: "Informazioni di Misskey"
aboutMisskey: "A proposito di Misskey"
administrator: "Amministratore"
token: "Token"
2fa: "Autenticazione a due fattori"
@ -893,7 +893,7 @@ searchResult: "Risultati della Ricerca"
hashtags: "Hashtag"
troubleshooting: "Risoluzione problemi"
useBlurEffect: "Utilizza effetto sfocatura"
learnMore: "Più dettagli"
learnMore: "Per saperne di più"
misskeyUpdated: "Misskey è stato aggiornato!"
whatIsNew: "Informazioni sull'aggiornamento"
translate: "Traduci"
@ -901,7 +901,7 @@ translatedFrom: "Traduzione da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
aiChanMode: "Modalità Ai"
devMode: "Modalità sviluppatori"
devMode: "Modalità sviluppo"
keepCw: "Mostra i contenuti espliciti"
pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
@ -1049,7 +1049,7 @@ permissionDeniedError: "Errore, attività non autorizzata"
permissionDeniedErrorDescription: "Non si dispone dell'autorizzazione per eseguire questa operazione."
preset: "Preimpostato"
selectFromPresets: "Seleziona preimpostato"
achievements: "Obiettivi raggiunti"
achievements: "Conquiste"
gotInvalidResponseError: "Risposta del server non valida"
gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi."
thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva"
@ -1311,6 +1311,8 @@ federationSpecified: "Questo server è federato solo con istanze specifiche del
federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
confirmOnReact: "Confermare le reazioni"
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?"
_accountSettings:
requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
@ -1447,9 +1449,9 @@ _initialTutorial:
description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}."
_timelineDescription:
home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (Following)."
local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server."
social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale."
global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo."
local: "La Timeline Locale è un flusso di Note pubblicate dai profili iscritti a questo server."
social: "La Timeline Sociale elenca, in ordine cronologico, il flusso di Note nella Timeline Home e Locale."
global: "Nella Timeline Federata trovi il flusso di Note provenienti da profili iscritti ad altri server, federati a questo."
_serverRules:
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
_serverSettings:
@ -1474,7 +1476,7 @@ _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto"
moveFromLabel: "Profilo da cui migrare #{n}"
moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@vecchia.istanza.it"
moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare"
moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
@ -1550,13 +1552,13 @@ _achievements:
title: "Principiante III"
description: "Hai totalizzato 15 accessi!"
_login30:
title: "Misskist I"
title: "Missalcolista I"
description: "Hai totalizzato 30 accessi!"
_login60:
title: "Misskeist II"
title: "Missalcolista II"
description: "Hai totalizzato 60 accessi!"
_login100:
title: "Misskeist III"
title: "Missalcolista III"
description: "Hai totalizzato 100 accessi!"
flavor: "Violent Misskeist"
_login200:
@ -1642,10 +1644,10 @@ _achievements:
description: "Hai superato i 1.000 profili Follower"
_collectAchievements30:
title: "Collezionista di successi"
description: "Hai raggiunto 30 obiettivi"
description: "Hai raggiunto 30 conquiste"
_viewAchievements3min:
title: "Mi piacciono i risultati"
description: "Guarda la tua collezione di obiettivi per almeno 3 minuti"
description: "Ammira la tua collezione di conquiste per almeno 3 minuti"
_iLoveMisskey:
title: "I LOVE Misskey"
description: "Pubblica «I ♥ #Misskey»"
@ -1910,7 +1912,7 @@ _registry:
domain: "Dominio"
createKey: "Crea chiave"
_aboutMisskey:
about: "Misskey è un software libero e open source, sviluppato da syuilo dal 2014."
about: "Misskey è software libero, open source, sviluppato da Syuilo fin dal lontano 2014."
contributors: "Principali sostenitori"
allContributors: "Tutti i sostenitori"
source: "Codice sorgente"
@ -2237,7 +2239,7 @@ _widgets:
userList: "Elenco utenti"
_userList:
chooseList: "Seleziona una lista"
clicker: "Cliccaggio"
clicker: "Cliccheria"
birthdayFollowings: "Compleanni del giorno"
_cw:
hide: "Nascondere"
@ -2300,7 +2302,7 @@ _profile:
metadataContent: "Contenuto"
changeAvatar: "Modifica immagine profilo"
changeBanner: "Cambia intestazione"
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo."
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo.\nPer verificare il profilo tramite la spunta di conferma, devi inserire la url alla pagina che contiene un link al tuo profilo Misskey. Deve avere attributo rel='me'."
avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
followedMessage: "Messaggio, quando qualcuno ti segue"
followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
@ -2594,6 +2596,7 @@ _moderationLogTypes:
deletePage: "Pagina eliminata"
deleteFlash: "Play eliminato"
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
_fileViewer:
title: "Dettagli del file"
type: "Tipo di file"
@ -2811,8 +2814,8 @@ _selfXssPrevention:
description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra."
description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}"
_followRequest:
recieved: "Ricezione richiesta di Follow"
sent: "Richiesta di Follow, inviata"
recieved: "Richieste in ingresso"
sent: "Richieste in uscita"
_remoteLookupErrors:
_federationNotAllowed:
title: "Server irraggiungibile"
@ -2857,4 +2860,8 @@ _bootErrors:
_search:
searchScopeAll: "Tutte"
searchScopeLocal: "Locale"
searchScopeServer: "Specifiche del server"
searchScopeUser: "Profilo specifico"
pleaseEnterServerHost: "Inserire il nome host"
pleaseSelectUser: "Per favore, seleziona un profilo"
serverHostPlaceholder: "Es: misskey.example.com"

View file

@ -1238,7 +1238,7 @@ releaseToRefresh: "離してリロード"
refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
useGroupedNotifications: "通知をグルーピングして表示する"
useGroupedNotifications: "通知をグルーピング"
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
doReaction: "リアクションする"
@ -1311,6 +1311,10 @@ federationSpecified: "このサーバーはホワイトリスト連合で運用
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
preferences: "環境設定"
accessibility: "アクセシビリティ"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@ -2665,6 +2669,7 @@ _moderationLogTypes:
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer:
title: "ファイルの詳細"

View file

@ -1311,6 +1311,8 @@ federationSpecified: "此服务器已开启联合白名单。只能与管理员
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
confirmOnReact: "发送回应前需要确认"
reactAreYouSure: "要用「{emoji}」进行回应吗?"
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@ -2594,6 +2596,7 @@ _moderationLogTypes:
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件"
updateProxyAccountDescription: "更新代理账户的说明"
_fileViewer:
title: "文件信息"
type: "文件类型"
@ -2857,4 +2860,8 @@ _bootErrors:
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
searchScopeUser: "用户指定"
searchScopeServer: "指定服务器"
searchScopeUser: "指定用户"
pleaseEnterServerHost: "请填写服务器主机名"
pleaseSelectUser: "请选择用户"
serverHostPlaceholder: "如misskey.example.com"

View file

@ -1311,6 +1311,8 @@ federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管
federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
confirmOnReact: "反應時確認"
reactAreYouSure: "用「 {emoji} 」反應嗎?"
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
@ -2594,6 +2596,7 @@ _moderationLogTypes:
deletePage: "刪除頁面"
deleteFlash: "刪除 Play"
deleteGalleryPost: "刪除相簿的貼文"
updateProxyAccountDescription: "更新代理帳戶的說明"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
@ -2857,4 +2860,8 @@ _bootErrors:
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
searchScopeServer: "指定伺服器"
searchScopeUser: "指定使用者"
pleaseEnterServerHost: "請輸入伺服器的主機名稱"
pleaseSelectUser: "請選擇使用者"
serverHostPlaceholder: "例misskey.example.com"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.2.1-yumechinokuni.0",
"version": "2025.3.0-yumechinokuni.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 && cross-env RUN_MODE=web node ./built/boot/entry.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test RUN_MODE=test node ./built/boot/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test RUN_MODE=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",

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts1741279404074 {
name = 'SystemAccounts1741279404074'
async up(queryRunner) {
const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'actor')`);
if (instanceActor.length > 0) {
console.warn('instance.actor was incorrect, updating...');
await queryRunner.query(`UPDATE "system_account" SET "id" = '${instanceActor[0].id}', "userId" = '${instanceActor[0].id}' WHERE "type" = 'actor'`);
}
const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'relay')`);
if (relayActor.length > 0) {
console.warn('relay.actor was incorrect, updating...');
await queryRunner.query(`UPDATE "system_account" SET "id" = '${relayActor[0].id}', "userId" = '${relayActor[0].id}' WHERE "type" = 'relay'`);
}
}
async down(queryRunner) {
// fixup migration, no down migration
}
}

View file

@ -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:

View file

@ -224,7 +224,7 @@ export type Config = {
redisForTimelines: RedisOptions & RedisOptionsSource;
redisForReactions: RedisOptions & RedisOptionsSource;
prometheusMetrics : { enable: boolean, scrapeToken?: string } | undefined;
prometheusMetrics: { enable: boolean, scrapeToken?: string } | undefined;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;

View file

@ -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);

View file

@ -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 } }]);
}
}

View file

@ -0,0 +1,32 @@
import crypto from 'node:crypto';
import { Inject, Injectable } from "@nestjs/common";
@Injectable()
export class ActorKeySignerService {
private proxyUri?: string;
constructor() {
this.proxyUri = process.env.MISSKEY_ACTOR_KEY_PROXY_URL;
}
public async sign(id: string, privateKey: string, data: string): Promise<string> {
if (this.proxyUri && privateKey === 'proxy') {
const response = await fetch(`${this.proxyUri}signature/${id}`, {
method: 'POST',
body: data,
});
if (!response.ok) {
throw new Error(`Failed to sign data: ${response.statusText}`);
}
return response.text();
}
const signer = crypto.createSign('sha256');
signer.update(data);
signer.end();
const signature = signer.sign(privateKey);
return signature.toString('base64');
}
}

View file

@ -23,7 +23,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';
@ -36,7 +35,7 @@ import { HashtagService } from './HashtagService.js';
import { HttpRequestService } from './HttpRequestService.js';
import { IdService } from './IdService.js';
import { __YUME_PRIVATE_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';
@ -68,7 +67,6 @@ import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { __YUME_PRIVATE_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';
@ -152,6 +150,7 @@ import { QueueModule } from './QueueModule.js';
import { QueueService } from './QueueService.js';
import { LoggerService } from './LoggerService.js';
import type { Provider } from '@nestjs/common';
import { ActorKeySignerService } from './ActorKeySignerService.js';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
@ -165,7 +164,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 };
@ -178,7 +176,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: '__YUME_PRIVATE_ImageProcessingService', useExisting: __YUME_PRIVATE_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 };
@ -189,7 +186,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 };
@ -315,7 +312,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
DownloadService,
@ -328,7 +324,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HttpRequestService,
IdService,
__YUME_PRIVATE_ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
MfmService,
@ -339,7 +334,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteReadService,
NotificationService,
PollService,
ProxyAccountService,
SystemAccountService,
PushNotificationService,
QueryService,
ReactionService,
@ -437,6 +432,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApMfmService,
ApRendererService,
ApRequestService,
ActorKeySignerService,
ApResolverService,
JsonLdService,
RemoteLoggerService,
@ -461,7 +457,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
$DownloadService,
@ -474,7 +469,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$HttpRequestService,
$IdService,
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MetaService,
$MfmService,
@ -485,7 +479,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteReadService,
$NotificationService,
$PollService,
$ProxyAccountService,
$SystemAccountService,
$PushNotificationService,
$QueryService,
$ReactionService,
@ -608,7 +602,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
DownloadService,
@ -621,7 +614,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HttpRequestService,
IdService,
__YUME_PRIVATE_ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
MfmService,
@ -632,7 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteReadService,
NotificationService,
PollService,
ProxyAccountService,
SystemAccountService,
PushNotificationService,
QueryService,
ReactionService,
@ -753,7 +745,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
$DownloadService,
@ -766,7 +757,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$HttpRequestService,
$IdService,
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MetaService,
$MfmService,
@ -777,7 +767,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteReadService,
$NotificationService,
$PollService,
$ProxyAccountService,
$SystemAccountService,
$PushNotificationService,
$QueryService,
$ReactionService,

View file

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

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { 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', {

View file

@ -182,18 +182,17 @@ export class HttpRequestService {
* @param isLocalAddressAllowed
*/
@bindThis
public getAgentByUrl(url: URL, bypassProxy = false): https.Agent {
if (url.protocol && url.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
url.protocol = 'https:';
if (url.port && url.port !== '443') {
throw new Error('Invalid port');
}
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
return this.https;
if (isLocalAddressAllowed) {
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
}
return url.protocol === 'http:' ? this.http : this.https;
} else {
return this.httpsAgent;
if (isLocalAddressAllowed && (!this.config.proxy)) {
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
}
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
}
}

View file

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

View file

@ -53,7 +53,7 @@ export class MetaService implements OnApplicationShutdown {
case 'metaUpdated': {
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) {

View file

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

View file

@ -4,53 +4,34 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { 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);

View file

@ -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));

View file

@ -46,6 +46,8 @@ export class S3Service {
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
requestChecksumCalculation: 'WHEN_REQUIRED',
responseChecksumValidation: 'WHEN_REQUIRED',
});
}

View file

@ -305,4 +305,4 @@ export class SearchService {
return notes.sort((a, b) => a.id > b.id ? -1 : 1);
}
}
}

View file

@ -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 };
}
}

View file

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

View file

@ -15,11 +15,11 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { 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 } }]);
}
}

View file

@ -22,8 +22,8 @@ export type UserWebhookPayload<T extends WebhookEventTypes> =
reaction: string,
}
note: Packed<'Note'>,
}:
T extends 'note' | 'reply' | 'renote' |'mention' ? {
} :
T extends 'note' | 'reply' | 'renote' | 'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {

View file

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

View file

@ -572,14 +572,13 @@ export class ApInboxService {
}
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
return 'skip: already deleted';
return 'skip: already deleted or actor not found';
}
const job = await this.queueService.createDeleteAccountJob(actor);
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
return `ok: queued ${job.name} ${job.id}`;
return 'ok';
}
@bindThis

View file

@ -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,
},
}, undefined);
}
@ -255,6 +258,38 @@ export class ApRendererService {
}, undefined);
}
@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 markOutgoing({
@ -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,
@ -599,7 +634,7 @@ export class ApRendererService {
to: ['https://www.w3.org/ns/activitystreams#Public'],
object,
published: new Date().toISOString(),
}, undefined);
}, undefined);
}
@bindThis

View file

@ -19,6 +19,7 @@ import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js';
import { ActorKeySignerService } from '../ActorKeySignerService.js';
type Request = {
url: string;
@ -39,7 +40,8 @@ type PrivateKey = {
};
export class ApRequestCreator {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
static async createSignedPost(signer: ActorKeySignerService, args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Promise<Signed> {
const u = new URL(args.url);
const digestHeader = args.digest ?? this.createDigest(args.body);
@ -54,7 +56,7 @@ export class ApRequestCreator {
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
const result = await this.#signToRequest(signer, request, args.key, ['(request-target)', 'date', 'host', 'digest']);
return {
request,
@ -68,7 +70,7 @@ export class ApRequestCreator {
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
}
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
static async createSignedGet(signer: ActorKeySignerService, args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Promise<Signed> {
const u = new URL(args.url);
const request: Request = {
@ -81,7 +83,7 @@ export class ApRequestCreator {
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
const result = await this.#signToRequest(signer, request, args.key, ['(request-target)', 'date', 'host', 'accept']);
return {
request,
@ -91,9 +93,9 @@ export class ApRequestCreator {
};
}
static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
static async #signToRequest(signer: ActorKeySignerService, request: Request, key: PrivateKey, includeHeaders: string[]): Promise<Signed> {
const signingString = this.#genSigningString(request, includeHeaders);
const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
const signature = await signer.sign(key.keyId, key.privateKeyPem, signingString);
const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
request.headers = this.#objectAssignWithLcKey(request.headers, {
@ -149,6 +151,7 @@ export class ApRequestService {
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
private utilityService: UtilityService,
private actorKeySignerService: ActorKeySignerService,
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
@ -160,7 +163,7 @@ export class ApRequestService {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedPost({
const req = await ApRequestCreator.createSignedPost(this.actorKeySignerService, {
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
@ -189,7 +192,7 @@ export class ApRequestService {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
const req = await ApRequestCreator.createSignedGet(this.actorKeySignerService, {
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,

View file

@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import * as Bull from 'bullmq';
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';
@ -16,14 +15,15 @@ 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, yumeNormalizeObject } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
import type { IObject, ICollection, IOrderedCollection, IUnsanitizedObject } from './type.js';
import { yumeAssertAcceptableURL } from './misc/validator.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
export class Resolver {
private history: Set<string>;
@ -39,7 +39,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,
@ -109,7 +109,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
@ -218,7 +218,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,
@ -238,7 +238,7 @@ export class ApResolverService {
this.noteReactionsRepository,
this.followRequestsRepository,
this.utilityService,
this.instanceActorService,
this.systemAccountService,
this.apRequestService,
this.httpRequestService,
this.apRendererService,

View file

@ -11,6 +11,7 @@ import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld';
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
import { ActorKeySignerService } from '../ActorKeySignerService.js';
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
@ -21,6 +22,7 @@ class JsonLd {
constructor(
private httpRequestService: HttpRequestService,
private actorKeySignerService: ActorKeySignerService,
) {
}
@ -45,17 +47,13 @@ class JsonLd {
const toBeSigned = await this.createVerifyData(data, options);
const signer = crypto.createSign('sha256');
signer.update(toBeSigned);
signer.end();
const signature = signer.sign(privateKey);
const signature = await this.actorKeySignerService.sign(creator, privateKey, toBeSigned);
return {
...data,
signature: {
...options,
signatureValue: signature.toString('base64'),
signatureValue: signature,
},
};
}
@ -169,11 +167,12 @@ class JsonLd {
export class JsonLdService {
constructor(
private httpRequestService: HttpRequestService,
private actorKeySignerService: ActorKeySignerService,
) {
}
@bindThis
public use(): JsonLd {
return new JsonLd(this.httpRequestService);
return new JsonLd(this.httpRequestService, this.actorKeySignerService);
}
}

View file

@ -129,7 +129,6 @@ export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IO
// at lease host need to match exactly (ActivityPub requirement)
if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) {
mApUrlValidationFailed?.inc({
reason: 'not_same_host',
id_host: idParsed.host,

View file

@ -700,7 +700,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;

View file

@ -12,12 +12,12 @@ import type { MiRemoteUser } from '@/models/User.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, isQuestion } from '../type.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject } from '../type.js';
import { yumeAssertAcceptableURL } from '../misc/validator.js';
import { toASCII } from 'punycode';
@Injectable()
export class ApQuestionService {
@ -38,6 +38,7 @@ export class ApQuestionService {
private apResolverService: ApResolverService,
private apLoggerService: ApLoggerService,
private utilityService: UtilityService,
) {
this.logger = this.apLoggerService.logger;
}
@ -75,13 +76,13 @@ export class ApQuestionService {
const uriIn = typeof value === 'string' ? value : value.id;
if (uriIn == null) throw new Error('uri is null');
// URIがこのサーバーを指しているならスキップ
const uri = yumeAssertAcceptableURL(uriIn);
if (toASCII(this.config.host) === uri.host) throw new Error('uri points local');
// URIがこのサーバーを指しているならスキップ
if (this.utilityService.isUriLocal(uri.href)) throw new Error('uri points local');
//#region このサーバーに既に登録されているか
const note = await this.notesRepository.findOneBy({ uri: uriIn });
const note = await this.notesRepository.findOneBy({ uri: uri.href });
if (note == null) throw new Error('Question is not registered');
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });

View file

@ -48,7 +48,7 @@ export interface IUnsanitizedObject extends Partial<MisskeyVendorKeys> {
attributedTo?: ApObject;
attachment?: any[];
inReplyTo?: any;
replies?: ICollection;
replies?: ICollection | string;
content?: string | null;
startTime?: Date;
endTime?: Date;
@ -122,10 +122,14 @@ export function yumeNormalizeObject(object: IUnsanitizedObject, depth = 0): IObj
}
if (object.replies) {
object.replies.first = object.replies.first ?
typeof object.replies.first === 'string' ? yumeNormalizeURL(object.replies.first) : yumeNormalizeObject(object.replies.first, depth + 1) : undefined;
object.replies.items = object.replies.items ?
typeof object.replies.items === 'string' ? yumeNormalizeURL(object.replies.items) : yumeNormalizeRecursive(object.replies.items, depth + 1) : undefined;
if (typeof object.replies === 'string') {
object.replies = yumeNormalizeURL(object.replies);
} else {
object.replies.first = object.replies.first ?
typeof object.replies.first === 'string' ? yumeNormalizeURL(object.replies.first) : yumeNormalizeObject(object.replies.first, depth + 1) : undefined;
object.replies.items = object.replies.items ?
typeof object.replies.items === 'string' ? yumeNormalizeURL(object.replies.items) : yumeNormalizeRecursive(object.replies.items, depth + 1) : undefined;
}
}
if (object.inReplyTo) {
@ -243,7 +247,7 @@ export const isPost = (object: IObject): object is IPost => {
return type != null && validPost.includes(type);
};
export interface IPost extends IObject{
export interface IPost extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
source?: {
content: string;

View file

@ -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
@ -148,14 +146,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,

View file

@ -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

View file

@ -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'),

View file

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

View file

@ -5,5 +5,6 @@
import { secureRndstr } from '@/misc/secure-rndstr.js';
// 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;

View file

@ -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,
})

View file

@ -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,

View file

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

View file

@ -185,12 +185,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,

View file

@ -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>;

View file

@ -85,6 +85,7 @@ import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache } from './misc/cache.js';
import { metricCounter, metricHistogram } from './server/api/MetricsService.js';
import { MiSystemAccount } from './models/SystemAccount.js';
pg.types.setTypeParser(20, Number);
@ -137,7 +138,6 @@ const mSlowQueryHisto = metricHistogram({
buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 300],
});
export type LoggerProps = {
disableQueryTruncation?: boolean;
enableQueryParamLogging?: boolean;
@ -293,6 +293,7 @@ export const entities = [
MiEmoji,
MiHashtag,
MiSwSubscription,
MiSystemAccount,
MiAbuseUserReport,
MiAbuseReportNotificationRecipient,
MiRegistrationTicket,

View file

@ -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';
import { IsNull, MoreThan, Not } from 'typeorm';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
@ -32,7 +32,7 @@ export class NodeinfoServerService {
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private userEntityService: UserEntityService,
private systemAccountService: SystemAccountService,
private metaService: MetaService,
private notesChart: NotesChart,
private usersChart: UsersChart,
@ -74,7 +74,7 @@ export class NodeinfoServerService {
this.notesRepository.count({ where: { userHost: IsNull(), replyId: Not(IsNull()) } }),
]);
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 };
@ -129,7 +129,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',
},
};

View file

@ -48,6 +48,7 @@ import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
import { MetricsService } from './api/MetricsService.js';
import { ActorKeySignerService } from '@/core/ActorKeySignerService.js';
@Module({
imports: [
@ -60,6 +61,7 @@ import { MetricsService } from './api/MetricsService.js';
FeedService,
HealthServerService,
UrlPreviewService,
ActorKeySignerService,
ActivityPubServerService,
FileServerService,
NodeinfoServerService,

View file

@ -371,7 +371,7 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
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)) {

View file

@ -12,7 +12,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';
import { metricCounter } from './MetricsService.js';
@ -54,7 +54,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>);

View file

@ -100,6 +100,7 @@ export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.
export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';
export * as 'admin/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';

View file

@ -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';
@ -65,19 +63,20 @@ 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 roleService: RoleService,
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) {
// 初期パスワードが設定されている場合
@ -89,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
throw new ApiError(meta.errors.wrongInitialPassword);
}
} else if (!(me?.isRoot) && !await this.roleService.isAdministrator(me)) {
} else if ((this.serverSettings.rootUserId !== me?.id) && !await this.roleService.isAdministrator(me)) {
// 管理者でない場合
throw new ApiError(meta.errors.accessDenied);
} else if (token && !token?.permission.includes('write:admin:create-account')) {

View file

@ -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);
});
}

View file

@ -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'],
@ -233,7 +234,7 @@ export const meta = {
},
proxyAccountId: {
type: 'string',
optional: false, nullable: true,
optional: false, nullable: false,
format: 'id',
},
email: {
@ -541,10 +542,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,
@ -608,7 +612,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,

View file

@ -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');
}

View file

@ -88,7 +88,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: {
@ -387,10 +386,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;
}

View file

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

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
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);

View file

@ -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,

View file

@ -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));
});
}

View file

@ -96,6 +96,7 @@ interface ClientInformation {
id: string;
redirectUris: string[];
name: string;
logo: string | null;
}
// https://indieauth.spec.indieweb.org/#client-information-discovery
@ -125,11 +126,19 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
redirectUris.push(...[...fragment.querySelectorAll<HTMLLinkElement>('link[rel=redirect_uri][href]')].map(el => el.href));
let name = id;
let logo: string | null = null;
if (text) {
const microformats = mf2(text, { baseUrl: res.url });
const nameProperty = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id))?.properties.name[0];
if (typeof nameProperty === 'string') {
name = nameProperty;
const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id));
if (correspondingProperties) {
const nameProperty = correspondingProperties.properties.name?.[0];
if (typeof nameProperty === 'string') {
name = nameProperty;
}
const logoProperty = correspondingProperties.properties.logo?.[0];
if (typeof logoProperty === 'string') {
logo = logoProperty;
}
}
}
@ -137,6 +146,7 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
id,
redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()),
name: typeof name === 'string' ? name : id,
logo,
};
} catch (err) {
console.error(err);
@ -380,6 +390,7 @@ export class OAuth2ProviderService {
return await reply.view('oauth', {
transactionId: oauth2.transactionID,
clientName: oauth2.client.name,
clientLogo: oauth2.client.logo,
scope: oauth2.req.scope.join(' '),
});
});

View file

@ -7,7 +7,6 @@ export class Systemd {
}
async start<T>(id: string, promise: Promise<T>): Promise<T> {
let state: {
state: 'running'
} | {
@ -45,7 +44,7 @@ export class Systemd {
div.appendChild(spanMessage);
return div;
}
};
const formatDone = () => {
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
@ -65,7 +64,7 @@ export class Systemd {
div.appendChild(spanMessage);
return div;
}
};
const formatFailed = (message: string) => {
const elapsed_secs = Math.floor((Date.now() - started) / 1000);
@ -85,7 +84,7 @@ export class Systemd {
div.appendChild(spanMessage);
return div;
}
};
const render = () => {
switch (state.state) {
@ -120,7 +119,7 @@ export class Systemd {
const interval = setInterval(render, 500);
try {
let res = await promise;
const res = await promise;
state = { state: 'done' };
return res;
} catch (e) {
@ -148,4 +147,4 @@ export class Systemd {
div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view logs.';
this.tty_dom.appendChild(div);
}
}
}

View file

@ -6,4 +6,6 @@ block meta
//- XXX: Remove navigation bar in auth page?
meta(name='misskey:oauth:transaction-id' content=transactionId)
meta(name='misskey:oauth:client-name' content=clientName)
if clientLogo
meta(name='misskey:oauth:client-logo' content=clientLogo)
meta(name='misskey:oauth:scope' content=scope)

View file

@ -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;

View file

@ -20,8 +20,12 @@ services:
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

View file

@ -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

View file

@ -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

View file

@ -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));
}

View file

@ -72,11 +72,12 @@ const clientConfig: ModuleOptions<'client_id'> = {
},
};
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } {
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } {
const fragment = JSDOM.fragment(html);
return {
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content,
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content,
clientLogo: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content,
};
}
@ -915,6 +916,59 @@ describe('OAuth', () => {
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
});
test('With Logo', async () => {
sender = (reply): void => {
reply.header('Link', '</redirect>; rel="redirect_uri"');
reply.send(`
<!DOCTYPE html>
<div class="h-app">
<a href="/" class="u-url p-name">Misklient</a>
<img src="/logo.png" class="u-logo" />
</div>
`);
reply.send();
};
const client = new AuthorizationCode(clientConfig);
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
} as AuthorizationParamsExtended));
assert.strictEqual(response.status, 200);
const meta = getMeta(await response.text());
assert.strictEqual(meta.clientName, 'Misklient');
assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`);
});
test('Missing Logo', async () => {
sender = (reply): void => {
reply.header('Link', '</redirect>; rel="redirect_uri"');
reply.send(`
<!DOCTYPE html>
<div class="h-app"><a href="/" class="u-url p-name">Misklient
`);
reply.send();
};
const client = new AuthorizationCode(clientConfig);
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
} as AuthorizationParamsExtended));
assert.strictEqual(response.status, 200);
const meta = getMeta(await response.text());
assert.strictEqual(meta.clientName, 'Misklient');
assert.strictEqual(meta.clientLogo, undefined);
});
test('Mismatching URL in h-app', async () => {
sender = (reply): void => {
reply.header('Link', '</redirect>; rel="redirect_uri"');

View file

@ -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,

View file

@ -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();

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 });

View file

@ -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() {

View file

@ -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' });

View file

@ -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() {

View file

@ -89,8 +89,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,

View file

@ -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]);

View file

@ -14,7 +14,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@tabler/icons-webfont": "3.31.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
@ -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"
}
}

View file

@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.13.5",
"@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.24.1",
"@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": [

View file

@ -39,6 +39,10 @@ const config = {
if (~replacePluginForIsChromatic) {
config.plugins?.splice(replacePluginForIsChromatic, 1);
}
//pluginsからcreateSearchIndexを削除、複数あるかもしれないので全て削除
config.plugins = config.plugins?.filter((plugin: Plugin) => plugin && plugin.name !== 'createSearchIndex') ?? [];
return mergeConfig(config, {
plugins: [
{

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.3.0/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.31.0/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style>
html {

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@tabler/icons-webfont": "3.31.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
@ -38,9 +38,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",
@ -49,6 +49,7 @@
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"magic-string": "0.30.17",
"matter-js": "0.20.0",
"mfm-js": "0.24.0",
"misskey-bubble-game": "workspace:*",
@ -56,84 +57,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"
}
}

View file

@ -34,7 +34,7 @@ defineProps<{
width: 100%;
height: 100%;
cursor: not-allowed;
--color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
}

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