Compare commits

..

22 Commits

Author SHA1 Message Date
syuilo
407a965c1d Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
2023-06-05 19:47:08 +09:00
syuilo
de6348e8a0 Merge pull request #10833 from misskey-dev/develop
* refactor(frontend): use css modules

* feat: 投稿したコンテンツのAIによる学習を軽減するオプションを追加

Resolve #10819

* enhance(backend): publicReactionsをデフォルトtrueに

* 念のためnoimageaiもつける

* add X-Robots-Tag: noai

* Update ja-JP.yml

* fix(frontend): ブラーエフェクトを有効にしている状態で高負荷になる問題を修正

* enhance(backend): graceful shutdown for job queue and refactor

* fix(backend): テスト時は一部のサービスを停止

* fix test

* New Crowdin updates (#10815)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* refactor

* bump

* refactor(frontend): use css module

* refactor(frontend): use css module

* delete unused component

* センシティブワードを正規表現、CWにも適用するように (#10688)

* cwにセンシティブが効いてない

* CWが無いときにTextを見るように

* 比較演算子間違えた

* とりあえずチェック

* 正規表現対応

* /test/giにも対応

* matchでしなくてもいいのでは感

* レビュー修正

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update packages/backend/src/core/NoteCreateService.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* 修正

* wipかも

* wordsでスペース区切りのものできたかも

* なんか動いたかも

* test作成

* 文言の修正

* 修正

* note参照

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update CHANGELOG.md

* New Crowdin updates (#10823)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* ci: fix typo

* fix(frontend): より明確な説明にしたのとtypo修正

* fix typo

* fix(frontend): カラーバーがリプライには表示されないのを修正

* fix(frontend): チャンネル内の検索ボックスが挙動不審な問題を修正

Fix #10793

* enhance(backend): ノートのハッシュタグもMeilisearchに突っ込むように

今後ハッシュタグ検索とか実装するときのため

* feat(frontend): ユーザー指定ノート検索

* fix(frontend): fix retention chart rendering

* Update about-misskey.vue

* meta: Remove @rinsuki from reviewer-lottery (#10830)

* New Crowdin updates (#10824)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Thai)

* enhance(frontend): アカウント初期設定ウィザードにプライバシー設定を追加

* Update CHANGELOG.md

* fix(backend): ひとつのMeilisearchサーバーを複数のMisskeyサーバーで使えない問題を修正

* fix MkUserSetupDialog.Privacy.vue

* ci: skip non-Japanese locale on TurboSnap

* ci: notify on changes for push events

* ci: fix missing branch

* Update basic.cy.js

* [ci skip] New Crowdin updates (#10834)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Arabic)

* 🎨

* 🎨

* enhance(frontend): add retention line chart

* update deps

* refactor

* fix(frontend): Pageにおいて画像ブロックに画像を設定できない問題を修正

Fix #10837

---------

Co-authored-by: nenohi <kimutipartylove@gmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: rinsuki <428rinsuki+git@gmail.com>
2023-05-12 12:41:53 +09:00
syuilo
9ad57324db Merge pull request #10814 from misskey-dev/develop
Release: 13.12.1
2023-05-09 15:38:17 +09:00
syuilo
94690c835e Merge pull request #10774 from misskey-dev/develop
Release: 13.12.0
2023-05-09 09:17:34 +09:00
syuilo
c5d2dba28d Merge pull request #10608 from misskey-dev/develop
Release: 13.11.3
2023-04-13 12:18:07 +09:00
syuilo
272e0c874f Merge pull request #10606 from misskey-dev/EbiseLutica-patch-1
Update CHANGELOG.md
2023-04-13 08:35:14 +09:00
Ebise Lutica
d429f810a9 Update CHANGELOG.md 2023-04-13 00:31:22 +09:00
syuilo
75b28d6782 Merge pull request #10578 from misskey-dev/develop
Release: 13.11.2
2023-04-11 15:51:07 +09:00
syuilo
8b1362ab03 Merge pull request #10543 from misskey-dev/develop
Release: 13.11.1
2023-04-09 10:29:36 +09:00
syuilo
a096f621cf Merge pull request #10506 from misskey-dev/develop
13.11.0
2023-04-08 21:27:21 +09:00
syuilo
f54a9542bb Merge pull request #10402 from misskey-dev/develop
Release: 13.10.3
2023-03-25 08:36:41 +09:00
syuilo
a52bbc7c8d Merge pull request #10388 from misskey-dev/develop
Release: 13.10.2
2023-03-22 18:47:10 +09:00
syuilo
59768bdf3f Merge pull request #10383 from misskey-dev/develop
Release: 13.10.1
2023-03-22 16:30:36 +09:00
syuilo
1e67e9c661 Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
2023-03-22 09:55:38 +09:00
syuilo
ae517a99a7 Merge pull request #10218 from misskey-dev/develop
Release: 13.9.2
2023-03-06 11:54:12 +09:00
syuilo
b23a9b1a88 Merge pull request #10181 from misskey-dev/develop
Release: 13.9.1
2023-03-03 20:56:50 +09:00
syuilo
5bd68aa3e0 Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
2023-03-03 15:35:40 +09:00
syuilo
647ce174b3 Merge pull request #10112 from misskey-dev/develop
Release: 13.8.1
2023-02-26 20:57:13 +09:00
syuilo
02c8fd9de5 Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)

* update PULL_REQUEST_TEMPLATE

* 起動時にRedisの疎通確認を行う (#9832)

* 起動時にRedisの疎通確認を行う

* check:connectをstart内に移動

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* Pass `--detectOpenHandles` to Jest (#9895)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* test(backend): restore ap-request tests (#9997)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* fix/refaftor(client): MkTime.vueの変更 (#10061)

* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)

* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする

* CIがない場合の処理追加

* enhance(client): improve clip menu ux

* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正

* 順番関係ある?

* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)

* 🎨

* feat: 2つの検索画面の統合 (#9949) (#10038)

* feat: 検索画面の UI を統一

* fix: エラーの修正

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): ノートメニューからユーザーメニューを開けるように

Resolve #10019

* enhance(client): renoteした際の表示を改善

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

* refactor(frontend): fix eslint error (#10084)

* Simplify search.vue (remove dead code) (#10088)

* Simplify search.vue

This is already handled by the code above it, no need to handle it twice

* Remove unused imports

* Update about-misskey.vue

* test(server): add validation test of api:notes/create (#10090)

* fix(server): notes/createのバリデーションが効いていない
Fix #10079

Co-Authored-By: mei23 <m@m544.net>

* anyOf内にバリデーションを書いても最初の一つしかチェックされない

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* New Crowdin updates (#10059)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): メニューの「もっと」からインスタンス情報を見れるように

* [Fix] fixed an typo in error message (#10102)

* Update codecov.yml

* Update CHANGELOG.md

* fix(server): エラーのスタックトレースは返さないように

Fix #10064

* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)

* Added yaml file in addition to yml file, in editorconfig

* Applied editorconfig for pnpm-workspace.yaml

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* update deps

* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)

* refactor

* New translations ja-JP.yml (French) (#10103)

* Update CHANGELOG.md

* 13.8.0

---------

Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-02-26 20:21:54 +09:00
syuilo
1ba49b614d Merge pull request #10058 from misskey-dev/develop
Release: 13.7.5
2023-02-24 13:06:55 +09:00
tamaina
40de14415c Release: 13.7.4
Merge pull request #10050 from misskey-dev/develop
2023-02-23 23:11:25 +09:00
tamaina
7c9330a02f Release: 13.7.3
Merge pull request #10048 from misskey-dev/develop
2023-02-23 22:15:56 +09:00
405 changed files with 5747 additions and 6314 deletions

View File

@@ -6,7 +6,7 @@
"features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
"ghcr.io/devcontainers/features/node:1": {
"version": "20.3.1"
"version": "18.16.0"
}
},
"forwardPorts": [3000],

View File

@@ -6,10 +6,6 @@ indent_size = 2
charset = utf-8
insert_final_newline = true
end_of_line = lf
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_style = space

View File

@@ -39,22 +39,8 @@ Please include errors from the developer console and/or server log files if you
<!-- Tell us where on the platform it happens -->
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
### 💻 Frontend
* Model and OS of the device(s):
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
* Browser:
<!-- Example: Chrome 113.0.5672.126 -->
* Server URL:
<!-- Example: misskey.io -->
* Misskey:
13.x.x
### 🛰 Backend (for server admin)
<!-- If you are using a managed service, put that after the version. -->
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
* Misskey: 13.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
Misskey version:
PostgreSQL version:
Redis version:
Your OS:
Your browser:

View File

@@ -37,7 +37,7 @@ jobs:
with:
version: 8
run_install: false
- name: Use Node.js 20.x
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version-file: '.node-version'

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
node-version: [20.x]
node-version: [18.x]
services:
postgres:

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
node-version: [20.x]
node-version: [18.x]
steps:
- uses: actions/checkout@v3.3.0
@@ -51,7 +51,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [20.x]
node-version: [18.x]
browser: [chrome]
services:

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [20.x]
node-version: [18.x]
steps:
- uses: actions/checkout@v3.3.0

3
.gitignore vendored
View File

@@ -64,6 +64,3 @@ temp
*.blend3
*.blend4
*.blend5
# VSCode addon
.favorites.json

View File

@@ -1 +1 @@
20.3.1
18.16.0

View File

@@ -12,51 +12,6 @@
-->
## 13.x.x (unreleased)
### General
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- 全体的なDBクエリのパフォーマンスを向上
## 13.13.2
### General
- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように
- ロールが付与されているユーザーリストを非公開にできるように
- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました
### Client
- Fix: タブがバックグラウンドでもstreamが切断されないように
### Server
- Fix: キャッシュが溜まり続けないように
## 13.13.1
### Client
- Fix: タブがアクティブな間はstreamが切断されないように
### Server
- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正
## 13.13.0
### General
@@ -133,12 +88,11 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
## 13.12.0
### NOTE
- Node.js 18.16.0以上が必要になりました
- Node.js 18.6.0以上が必要になりました
### General
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
- Meilisearchを全文検索に使用できるようになりました
* 「フォロワーのみ」の投稿は検索結果に表示されません。
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.3.1-bullseye
ARG NODE_VERSION=18.16.0-bullseye
# build assets & compile TypeScript

View File

@@ -991,7 +991,7 @@ postToTheChannel: "In Kanal senden"
cannotBeChangedLater: "Kann später nicht mehr geändert werden."
reactionAcceptance: "Reaktionsannahme"
likeOnly: "Nur \"Gefällt mir\""
likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)"
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
nonSensitiveOnly: "Keine Sensitiven"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
rolesAssignedToMe: "Mir zugewiesene Rollen"
@@ -1062,7 +1062,6 @@ later: "Später"
goToMisskey: "Zu Misskey"
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
installed: "Installiert"
branding: "Branding"
_initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@@ -1094,7 +1093,7 @@ _accountMigration:
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich."
postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen."
movedTo: "Neues Konto:"
movedTo: "Umzugsziel:"
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@@ -1348,7 +1347,7 @@ _role:
condition: "Bedingung"
isConditionalRole: "Dies ist eine konditionale Rolle."
isPublic: "Öffentliche Rolle"
descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt."
descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt."
options: "Optionen"
policies: "Richtlinien"
baseRole: "Rollenvorlage"
@@ -1357,8 +1356,8 @@ _role:
iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
isExplorable: "Benutzerliste veröffentlichen"
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich."
isExplorable: "Rollenchronik veröffentlichen"
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht."
displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"

View File

@@ -991,7 +991,7 @@ postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later."
reactionAcceptance: "Reaction Acceptance"
likeOnly: "Only likes"
likeOnlyForRemote: "All (Only likes for remote instances)"
likeOnlyForRemote: "Only likes for remote instances"
nonSensitiveOnly: "Non-sensitive only"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
rolesAssignedToMe: "Roles assigned to me"
@@ -1062,7 +1062,6 @@ later: "Later"
goToMisskey: "To Misskey"
additionalEmojiDictionary: "Additional emoji dictionaries"
installed: "Installed"
branding: "Branding"
_initialAccountSetting:
accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."
@@ -1094,7 +1093,7 @@ _accountMigration:
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
movedTo: "New account:"
movedTo: "Account to move to:"
_achievements:
earnedAt: "Unlocked at"
_types:
@@ -1348,7 +1347,7 @@ _role:
condition: "Condition"
isConditionalRole: "This is a conditional role."
isPublic: "Public role"
descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users."
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
options: "Options"
policies: "Policies"
baseRole: "Role template"
@@ -1357,8 +1356,8 @@ _role:
iconUrl: "Icon URL"
asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Make role explorable"
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
isExplorable: "Role timeline is public"
descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set."
displayOrder: "Position"
descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role"

11
locales/index.d.ts vendored
View File

@@ -139,10 +139,8 @@ export interface Locale {
"suspendConfirm": string;
"unsuspendConfirm": string;
"selectList": string;
"editList": string;
"selectChannel": string;
"selectAntenna": string;
"editAntenna": string;
"selectWidget": string;
"editWidgets": string;
"editWidgetsExit": string;
@@ -316,7 +314,7 @@ export interface Locale {
"rename": string;
"avatar": string;
"banner": string;
"displayOfSensitiveMedia": string;
"nsfw": string;
"whenServerDisconnected": string;
"disconnectedFromServer": string;
"reload": string;
@@ -1067,10 +1065,6 @@ export interface Locale {
"goToMisskey": string;
"additionalEmojiDictionary": string;
"installed": string;
"branding": string;
"enableServerMachineStats": string;
"enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string;
"_initialAccountSetting": {
"accountCreated": string;
"letsStartAccountSetup": string;
@@ -1531,7 +1525,6 @@ export interface Locale {
"back": string;
"reduceFrequencyOfThisAd": string;
"hide": string;
"timezoneinfo": string;
};
"_forgotPassword": {
"enterEmail": string;
@@ -1593,7 +1586,7 @@ export interface Locale {
"morePatrons": string;
"patrons": string;
};
"_displayOfSensitiveMedia": {
"_nsfw": {
"respect": string;
"ignore": string;
"force": string;

View File

@@ -112,7 +112,7 @@ pinnedNote: "ピン留めされたノート"
pinned: "ピン留め"
you: "あなた"
clickToShow: "クリックして表示"
sensitive: "センシティブ"
sensitive: "閲覧注意"
add: "追加"
reaction: "リアクション"
reactions: "リアクション"
@@ -120,8 +120,8 @@ reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する"
attachCancel: "添付取り消し"
markAsSensitive: "センシティブとして設定"
unmarkAsSensitive: "センシティブを解除する"
markAsSensitive: "閲覧注意にする"
unmarkAsSensitive: "閲覧注意を解除する"
enterFileName: "ファイル名を入力"
mute: "ミュート"
unmute: "ミュート解除"
@@ -136,10 +136,8 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
editList: "リストを編集"
selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集"
selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了"
@@ -313,7 +311,7 @@ copyUrl: "URLをコピー"
rename: "名前を変更"
avatar: "アイコン"
banner: "バナー"
displayOfSensitiveMedia: "センシティブなメディアの表示"
nsfw: "閲覧注意"
whenServerDisconnected: "サーバーとの接続が失われたとき"
disconnectedFromServer: "サーバーから切断されました"
reload: "リロード"
@@ -695,7 +693,7 @@ driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
@@ -922,8 +920,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
beta: "ベータ"
enableAutoSensitive: "自動センシティブ判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
navbar: "ナビゲーションバー"
shuffle: "シャッフル"
@@ -1064,10 +1062,6 @@ later: "あとで"
goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
@@ -1357,8 +1351,8 @@ _role:
conditional: "コンディショナル"
condition: "条件"
isConditionalRole: "これはコンディショナルロールです。"
isPublic: "公開ロール"
descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。"
isPublic: "ロールを公開"
descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。"
options: "オプション"
policies: "ポリシー"
baseRole: "ベースロール"
@@ -1367,8 +1361,8 @@ _role:
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
isExplorable: "ユーザーを見つけやすくする"
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
isExplorable: "ロールタイムラインを公開"
descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
@@ -1417,7 +1411,7 @@ _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
sensitivity: "検出感度"
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
setSensitiveFlagAutomatically: "センシティブフラグを設定する"
setSensitiveFlagAutomatically: "NSFWフラグを設定する"
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
analyzeVideos: "動画の解析を有効化"
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
@@ -1451,7 +1445,6 @@ _ad:
back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
@@ -1511,9 +1504,9 @@ _aboutMisskey:
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者"
_displayOfSensitiveMedia:
respect: "センシティブ設定されたメディア隠す"
ignore: "センシティブ設定されたメディアを隠さない"
_nsfw:
respect: "閲覧注意のメディア隠す"
ignore: "閲覧注意のメディアを隠さない"
force: "常にメディアを隠す"
_instanceTicker:

View File

@@ -792,7 +792,6 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで"
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
configure: "設定する"
postToGallery: "ギャラリーへ投稿"
postToHashtag: "このハッシュタグで投稿"
gallery: "ギャラリー"
recentPosts: "最近の投稿"
popularPosts: "人気の投稿"
@@ -826,7 +825,6 @@ translatedFrom: "{x}から翻訳するで"
accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
aiChanMode: "藍モードやで"
devMode: "開発者モード"
keepCw: "CWを維持するで"
pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信"
@@ -836,8 +834,6 @@ breakFollow: "フォロワーを解除するで"
breakFollowConfirm: "フォロワー解除してもええか?"
itsOn: "オンになっとるよ"
itsOff: "オフになってるで"
on: "オン"
off: "オフ"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
unread: "未読"
filter: "フィルタ"
@@ -992,8 +988,6 @@ cannotBeChangedLater: "後からは変えられへんで。"
reactionAcceptance: "ツッコミの受け入れ"
likeOnly: "いいねだけ"
likeOnlyForRemote: "リモートからはいいねだけな"
nonSensitiveOnly: "センシティブじゃないやつだけ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語"
@@ -1051,17 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション"
specifyUser: "ユーザー指定"
failedToPreviewUrl: "プレビューできへん"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。"
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
changeReactionConfirm: "ツッコミを別のに変えるか?"
later: "あとで"
goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
_initialAccountSetting:
accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。"
@@ -1076,7 +1063,6 @@ _initialAccountSetting:
haveFun: "{name}、楽しんでな~"
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
skipAreYouSure: "初期設定飛ばすか?"
laterAreYouSure: "初期設定あとでやり直すん?"
_serverRules:
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
_accountMigration:

View File

@@ -1,7 +1,6 @@
---
_lang_: "Türkçe"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün"
search: "Arama"
notifications: "Bildirim"
@@ -14,9 +13,7 @@ cancel: "İptal"
enterUsername: "Kullanıcı adınızı giriniz"
noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu"
settings: "Ayarlar"
notificationSettings: "Bildirim Ayarları"
basicSettings: "Temel Ayarlar"
otherSettings: "Diğer Ayarlar"
openInWindow: "Bir pencere ile aç"
@@ -24,11 +21,9 @@ profile: "Profil"
timeline: "Zaman çizelgesi"
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı"
login: "Giriş Yap "
loggingIn: "Oturum aç"
logout: ıkış Yap"
signup: "Kayıt Ol"
uploading: "Yükleniyor"
save: "Kaydet"
users: "Kullanıcı"
addUser: "Kullanıcı Ekle"
favorite: "Favoriler"
@@ -36,7 +31,6 @@ favorites: "Favoriler"
unfavorite: "Favorilerden Kaldır"
favorited: "Favorilerime eklendi."
alreadyFavorited: "Zaten favorilerinizde kayıtlı."
cantFavorite: "Favorilere kayıt yapılamadı"
pin: "Sabitlenmiş"
unpin: "Sabitlemeyi kaldır"
copyContent: "İçeriği kopyala"
@@ -46,88 +40,23 @@ deleteAndEdit: "Sil ve yeniden düzenle"
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
addToList: "Listeye ekle"
sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "KullanıcıyıKopyala"
copyNoteId: "Kimlik notunu kopyala"
searchUser: "Kullanıcıları ara"
reply: "yanıt"
loadMore: "Devamını yükle"
showMore: "Devamını yükle"
lists: "Listeler"
noLists: "Liste yok"
note: "not"
notes: "notlar"
following: "takipçi"
followers: "takipçi"
followsYou: "seni takip ediyor"
createList: "Liste oluştur"
manageLists: "Yönetici Listeleri"
error: "hata"
follow: "takipçi"
followRequest: "Takip isteği"
followRequests: "Takip istekleri"
unfollow: "takip etmeyi bırak"
followRequestPending: "Bekleyen Takip Etme Talebi"
enterEmoji: "Emoji Giriniz"
renote: "vazgeçme"
unrenote: "not alma"
renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim"
quote: "alıntı"
pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş"
you: "sen"
unmute: "sesi aç"
renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et"
block: "engelle"
unblock: "engellemeyi kaldır"
suspend: "askıya al"
unsuspend: "askıya alma"
blockConfirm: "Onayı engelle"
unblockConfirm: "engellemeyi kaldır onayla"
selectChannel: "Kanal seç"
flagAsBot: "Bot olarak işaretle"
instances: "Sunucu"
remove: "Sil"
pinnedNotes: "Sabitlenen"
userList: "Listeler"
smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre"
user: "Kullanıcı"
searchByGoogle: "Arama"
_theme:
keys:
renote: "vazgeçme"
_sfx:
note: "notlar"
notification: "Bildirim"
_widgets:
profile: "Profil"
notifications: "Bildirim"
timeline: "Zaman çizelgesi"
_cw:
show: "Devamını yükle"
_visibility:
followers: "takipçi"
_profile:
username: "Kullanıcı Adı"
_exportOrImport:
followingList: "takipçi"
blockingList: "engelle"
userLists: "Listeler"
_notification:
_types:
follow: "takipçi"
renote: "vazgeçme"
quote: "alıntı"
_actions:
reply: "yanıt"
renote: "vazgeçme"
_deck:
_columns:
notifications: "Bildirim"
tl: "Zaman çizelgesi"
list: "Listeler"

View File

@@ -1060,7 +1060,6 @@ cancelReactionConfirm: "要取消回应吗?"
changeReactionConfirm: "要更改回应吗?"
later: "一会再说"
goToMisskey: "去往Misskey"
additionalEmojiDictionary: "表情符号追加字典"
installed: "已安装"
_initialAccountSetting:
accountCreated: "账户创建完成了!"

View File

@@ -1062,7 +1062,6 @@ later: "稍後再說"
goToMisskey: "往Misskey"
additionalEmojiDictionary: "表情符號的附加辭典"
installed: "已安裝"
branding: "品牌宣傳"
_initialAccountSetting:
accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.14.0-beta.1",
"version": "13.13.0",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build",
"watch": "pnpm dev",
"dev": "node ./scripts/dev.mjs",
"dev": "node ./scripts/dev.js",
"lint": "pnpm -r lint",
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
@@ -44,23 +44,23 @@
"lodash": "4.17.21"
},
"dependencies": {
"execa": "7.1.1",
"execa": "5.1.1",
"gulp": "4.0.2",
"gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0",
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "5.1.6"
"typescript": "5.1.3"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"cross-env": "7.0.3",
"cypress": "12.17.0",
"eslint": "8.44.0",
"cypress": "12.13.0",
"eslint": "8.41.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View File

@@ -17,7 +17,7 @@
"paths": {
"@/*": ["*"]
},
"target": "es2022"
"target": "es2021"
},
"minify": false
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,9 +0,0 @@
export class ad1677054292210 {
name = 'ad1677054292210';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
}
}

View File

@@ -1,17 +0,0 @@
export class ErrorImageUrl1685973839966 {
name = 'ErrorImageUrl1685973839966'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`);
}
}

View File

@@ -1,13 +0,0 @@
export class AddMetaOptions1688280713783 {
name = 'AddMetaOptions1688280713783'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`);
}
}

View File

@@ -3,9 +3,6 @@
"main": "./index.js",
"private": true,
"type": "module",
"engines": {
"node": ">=18.16.0"
},
"scripts": {
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js",
@@ -54,36 +51,35 @@
"utf-8-validate": "^6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.367.0",
"@aws-sdk/lib-storage": "3.367.0",
"@aws-sdk/node-http-handler": "3.360.0",
"@bull-board/api": "5.6.0",
"@bull-board/fastify": "5.6.0",
"@bull-board/ui": "5.6.0",
"@aws-sdk/client-s3": "3.321.1",
"@aws-sdk/lib-storage": "3.321.1",
"@aws-sdk/node-http-handler": "3.321.1",
"@bull-board/api": "5.2.0",
"@bull-board/fastify": "5.2.0",
"@bull-board/ui": "5.2.0",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0",
"@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.0",
"@fastify/http-proxy": "9.1.0",
"@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.2",
"@fastify/view": "8.0.0",
"@nestjs/common": "10.0.5",
"@nestjs/core": "10.0.5",
"@nestjs/testing": "10.0.5",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.4.2",
"@nestjs/core": "9.4.2",
"@nestjs/testing": "9.4.2",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0",
"@sinonjs/fake-timers": "10.2.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.68",
"@swc/core": "1.3.61",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"async-mutex": "^0.4.0",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bullmq": "4.2.0",
"cacheable-lookup": "7.0.0",
"bullmq": "3.15.0",
"cacheable-lookup": "6.1.0",
"cbor": "9.0.0",
"chalk": "5.2.0",
"chalk-template": "0.4.0",
@@ -94,24 +90,23 @@
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.19.2",
"fastify": "4.17.0",
"feed": "4.2.2",
"file-type": "18.5.0",
"file-type": "18.4.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "13.0.0",
"happy-dom": "10.0.3",
"got": "12.6.0",
"happy-dom": "9.20.3",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0",
"is-svg": "5.0.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "22.1.0",
"json5": "2.2.3",
"jsonld": "8.2.0",
"jsrsasign": "10.8.6",
"meilisearch": "0.33.0",
"meilisearch": "0.32.5",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
@@ -122,9 +117,10 @@
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "9.1.3",
"otpauth": "9.1.2",
"parse5": "7.1.2",
"pg": "8.11.1",
"pg": "8.11.0",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
@@ -133,39 +129,41 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.19.1",
"re2": "1.19.0",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"rndstr": "1.0.0",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"s-age": "1.1.2",
"sanitize-html": "2.11.0",
"semver": "7.5.3",
"sanitize-html": "2.10.0",
"seedrandom": "3.0.5",
"semver": "7.5.1",
"sharp": "0.32.1",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.6",
"systeminformation": "5.17.16",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.7",
"tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
"typescript": "5.1.6",
"typeorm": "0.3.16",
"typescript": "5.1.3",
"ulid": "2.3.0",
"unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.6.3",
"web-push": "3.6.1",
"ws": "8.13.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.6.1",
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.26",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
@@ -178,18 +176,17 @@
"@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.9",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31",
"@types/node": "20.4.0",
"@types/node": "20.2.5",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1",
"@types/pg": "8.10.2",
"@types/pg": "8.10.1",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.1",
"@types/qrcode": "1.5.0",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
@@ -201,19 +198,19 @@
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6",
"@types/uuid": "9.0.2",
"@types/uuid": "9.0.1",
"@types/vary": "1.1.0",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"aws-sdk-client-mock": "3.0.0",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3",
"eslint": "8.44.0",
"eslint": "8.41.0",
"eslint-plugin-import": "2.27.5",
"execa": "7.1.1",
"jest": "29.6.1",
"jest-mock": "29.6.1"
"execa": "6.1.0",
"jest": "29.5.0",
"jest-mock": "29.5.0"
}
}

View File

@@ -96,6 +96,12 @@ function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs');
nodejsLogger.info(`Version ${process.version} detected.`);
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
if (semver.lt(process.version, minVersion)) {
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
process.exit(1);
}
}
function loadConfigBoot(): Config {

View File

@@ -4,7 +4,6 @@ import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import * as nsfw from 'nsfwjs';
import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@@ -18,7 +17,6 @@ let isSupportedCpu: undefined | boolean = undefined;
@Injectable()
export class AiService {
private model: nsfw.NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor(
@Inject(DI.config)
@@ -41,13 +39,7 @@ export class AiService {
const tf = await import('@tensorflow/tfjs-node');
if (this.model == null) {
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
}
});
}
if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as any;

View File

@@ -32,6 +32,11 @@ export class AppLockService {
return this.lock(`ap-object:${uri}`, timeout);
}
@bindThis
public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`instance:${host}`, timeout);
}
@bindThis
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
return this.lock(`chart-insert:${lockKey}`, timeout);

View File

@@ -168,17 +168,6 @@ export class CacheService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.userByIdCache.dispose();
this.localUserByNativeTokenCache.dispose();
this.localUserByIdCache.dispose();
this.uriPersonCache.dispose();
this.userProfileCache.dispose();
this.userMutingsCache.dispose();
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowingChannelsCache.dispose();
}
@bindThis

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
@@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
@@ -349,14 +349,4 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
import * as stream from 'node:stream';
import * as util from 'node:util';
import { Inject, Injectable } from '@nestjs/common';
import ipaddr from 'ipaddr.js';
import IPCIDR from 'ip-cidr';
import PrivateIp from 'private-ip';
import chalk from 'chalk';
import got, * as Got from 'got';
import { parse } from 'content-disposition';
@@ -139,14 +140,13 @@ export class DownloadService {
@bindThis
private isPrivateIp(ip: string): boolean {
const parsedIp = ipaddr.parse(ip);
for (const net of this.config.allowedPrivateNetworks ?? []) {
if (parsedIp.match(ipaddr.parseCIDR(net))) {
const cidr = new IPCIDR(net);
if (cidr.contains(ip)) {
return false;
}
}
return parsedIp.range() !== 'unicast';
return PrivateIp(ip) ?? false;
}
}

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
@@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService implements OnApplicationShutdown {
export class FederatedInstanceService {
public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor(
@@ -77,14 +77,4 @@ export class FederatedInstanceService implements OnApplicationShutdown {
this.federatedInstanceCache.set(result.host, result);
}
@bindThis
public dispose(): void {
this.federatedInstanceCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2';
import type { Instance } from '@/models/entities/Instance.js';
import type { InstancesRepository } from '@/models/index.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js';
@@ -10,7 +12,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom';
import * as Redis from 'ioredis';
type NodeInfo = {
openRegistrations?: unknown;
@@ -36,43 +37,33 @@ export class FetchInstanceMetadataService {
private logger: Logger;
constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private appLockService: AppLockService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService,
@Inject(DI.redis)
private redisClient: Redis.Redis,
) {
this.logger = this.loggerService.getLogger('metadata', 'cyan');
}
@bindThis
public async tryLock(host: string): Promise<boolean> {
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
}
@bindThis
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
}
@bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
try {
const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host);
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);
const _instance = await this.instancesRepository.findOneBy({ host: instance.host });
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse
unlock();
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`);
try {
const [info, dom, manifest] = await Promise.all([
this.fetchNodeinfo(instance).catch(() => null),
this.fetchDom(instance).catch(() => null),
@@ -113,7 +104,7 @@ export class FetchInstanceMetadataService {
} catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally {
await this.unlock(host);
unlock();
}
}

View File

@@ -304,11 +304,11 @@ export class FileInfoService {
@bindThis
public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686
if (mime === 'audio/x-flac') {
return 'audio/flac';
if (mime === "audio/x-flac") {
return "audio/flac";
}
if (mime === 'audio/vnd.wave') {
return 'audio/wav';
if (mime === "audio/vnd.wave") {
return "audio/wav";
}
return mime;
@@ -355,12 +355,11 @@ export class FileInfoService {
* Check the file is SVG or not
*/
@bindThis
public async checkSvg(path: string): Promise<boolean> {
public async checkSvg(path: string) {
try {
const size = await this.getFileSize(path);
if (size > 1 * 1024 * 1024) return false;
const buffer = await fs.promises.readFile(path);
return isSvg(buffer.toString());
return isSvg(fs.readFileSync(path));
} catch {
return false;
}

View File

@@ -20,7 +20,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { Role } from '@/models/index.js';
import { Role } from '@/models';
@Injectable()
export class GlobalEventService {

View File

@@ -5,7 +5,7 @@ import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
import { genObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js';
@@ -38,7 +38,7 @@ export class IdService {
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
case 'objectid': return parseObjectId(id);
case 'objectid':
case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id);
case 'ulid': return parseUlid(id);

View File

@@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { KEYWORD } from 'color-convert/conversions.js';
import type { KEYWORD } from 'color-convert/conversions';
@Injectable()
export class LoggerService {

View File

@@ -121,8 +121,10 @@ export class NoteDeleteService {
}
@bindThis
private async findCascadingNotes(note: Note): Promise<Note[]> {
const recursive = async (noteId: string): Promise<Note[]> => {
private async findCascadingNotes(note: Note) {
const cascadingNotes: Note[] = [];
const recursive = async (noteId: string) => {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
@@ -131,14 +133,12 @@ export class NoteDeleteService {
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
return [
replies,
...await Promise.all(replies.map(reply => recursive(reply.id))),
].flat();
for (const reply of replies) {
cascadingNotes.push(reply);
await recursive(reply.id);
}
};
const cascadingNotes: Note[] = await recursive(note.id);
await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
}

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js';
@@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
}
@Injectable()
export class PushNotificationService implements OnApplicationShutdown {
export class PushNotificationService {
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
constructor(
@@ -115,14 +115,4 @@ export class PushNotificationService implements OnApplicationShutdown {
});
}
}
@bindThis
public dispose(): void {
this.subscriptionsCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
@@ -69,7 +69,7 @@ export class QueueService {
if (content == null) return null;
if (to == null) return null;
const data: DeliverJobData = {
const data = {
user: {
id: user.id,
},
@@ -88,40 +88,6 @@ export class QueueService {
});
}
/**
* ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
* @param content IActivity | null
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
* @returns void
*/
@bindThis
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
if (content == null) return null;
const opts = {
attempts: this.config.deliverJobMaxAttempts ?? 12,
backoff: {
type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
};
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
name: d[0],
data: {
user,
content,
to: d[0],
isSharedInbox: d[1],
} as DeliverJobData,
opts,
})));
return;
}
@bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
const data = {
@@ -434,11 +400,11 @@ export class QueueService {
this.deliverQueue.once('cleaned', (jobs, status) => {
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
this.deliverQueue.clean(0, 0, 'delayed');
this.deliverQueue.clean(0, Infinity, 'delayed');
this.inboxQueue.once('cleaned', (jobs, status) => {
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
this.inboxQueue.clean(0, 0, 'delayed');
this.inboxQueue.clean(0, Infinity, 'delayed');
}
}

View File

@@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Packed } from '@/misc/json-schema';
import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
@@ -435,7 +435,6 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.roleAssignmentByUserIdCache.dispose();
}
@bindThis

View File

@@ -174,7 +174,7 @@ export class SearchService {
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
return await query.limit(pagination.limit).getMany();
return await query.take(pagination.limit).getMany();
}
}
}

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairService implements OnApplicationShutdown {
export class UserKeypairService {
private cache: RedisKVCache<UserKeypair>;
constructor(
@@ -31,14 +31,4 @@ export class UserKeypairService implements OnApplicationShutdown {
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId);
}
@bindThis
public dispose(): void {
this.cache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -29,7 +30,7 @@ export type UriParseResult = {
};
@Injectable()
export class ApDbResolverService implements OnApplicationShutdown {
export class ApDbResolverService {
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
@@ -55,18 +56,25 @@ export class ApDbResolverService implements OnApplicationShutdown {
@bindThis
public parseUri(value: string | IObject): UriParseResult {
const separator = '/';
const uri = getApId(value);
const uri = new URL(getApId(value));
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
const matchLocal = uri.match(localRegex);
const [, type, id, ...rest] = uri.pathname.split(separator);
if (matchLocal) {
return {
local: true,
type,
id,
rest: rest.length === 0 ? undefined : rest.join(separator),
type: matchLocal[1],
id: matchLocal[2],
rest: matchLocal[3],
};
} else {
return {
local: false,
uri,
};
}
}
/**
@@ -154,15 +162,4 @@ export class ApDbResolverService implements OnApplicationShutdown {
key,
};
}
@bindThis
public dispose(): void {
this.publicKeyCache.dispose();
this.publicKeyByUserIdCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -7,8 +7,6 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { QueueService } from '@/core/QueueService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js';
interface IRecipe {
type: string;
@@ -23,10 +21,10 @@ interface IDirectRecipe extends IRecipe {
to: RemoteUser;
}
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
recipe.type === 'Followers';
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
const isDirect = (recipe: any): recipe is IDirectRecipe =>
recipe.type === 'Direct';
@Injectable()
@@ -48,11 +46,11 @@ export class ApDeliverManagerService {
/**
* Deliver activity to followers
* @param actor
* @param activity Activity
* @param from Followee
*/
@bindThis
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
@@ -66,12 +64,11 @@ export class ApDeliverManagerService {
/**
* Deliver activity to user
* @param actor
* @param activity Activity
* @param to Target user
*/
@bindThis
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
@@ -84,7 +81,7 @@ export class ApDeliverManagerService {
}
@bindThis
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) {
return new DeliverManager(
this.userEntityService,
this.followingsRepository,
@@ -97,15 +94,12 @@ export class ApDeliverManagerService {
}
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
private actor: { id: User['id']; host: null; };
private activity: any;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
@@ -115,15 +109,9 @@ class DeliverManager {
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: IActivity | null,
activity: any,
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.actor = actor;
this.activity = activity;
}
@@ -167,8 +155,9 @@ class DeliverManager {
*/
@bindThis
public async execute() {
if (!this.userEntityService.isLocalUser(this.actor)) return;
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
/*
@@ -212,6 +201,9 @@ class DeliverManager {
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
for (const inbox of inboxes) {
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
}
}
}

View File

@@ -10,10 +10,9 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
import type { IObject } from '../type.js';
import { checkHttps } from '@/misc/check-https.js';
@Injectable()
export class ApImageService {
@@ -38,22 +37,18 @@ export class ApImageService {
* Imageを作成します。
*/
@bindThis
public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
}
const image = await this.apResolverService.createResolver().resolve(value);
const image = await this.apResolverService.createResolver().resolve(value) as any;
if (image.url == null) {
throw new Error('invalid image: url not privided');
}
if (typeof image.url !== 'string') {
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
}
if (!checkHttps(image.url)) {
throw new Error('invalid image: unexpected schema of url: ' + image.url);
}
@@ -62,19 +57,29 @@ export class ApImageService {
const instance = await this.metaService.fetch();
const file = await this.driveService.uploadFromUrl({
let file = await this.driveService.uploadFromUrl({
url: image.url,
user: actor,
uri: image.url,
sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles,
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
});
if (!file.isLink || file.url === image.url) return file;
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する
await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
// URLを更新する
if (file.url !== image.url) {
await this.driveFilesRepository.update({ id: file.id }, {
url: image.url,
uri: image.url,
});
file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
}
}
return file;
}
/**
@@ -84,7 +89,7 @@ export class ApImageService {
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
@bindThis
public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
// TODO
// リモートサーバーからフェッチしてきて登録

View File

@@ -22,8 +22,8 @@ export class ApMentionService {
}
@bindThis
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all(

View File

@@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js';
@@ -71,9 +72,13 @@ export class ApNoteService {
}
@bindThis
public validateNote(object: IObject, uri: string): Error | null {
public validateNote(object: IObject, uri: string) {
const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) {
return new Error('invalid Note: object is null');
}
if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
}
@@ -105,7 +110,6 @@ export class ApNoteService {
*/
@bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value);
@@ -113,10 +117,12 @@ export class ApNoteService {
const entryUri = getApId(value);
const err = this.validateNote(object, entryUri);
if (err) {
this.logger.error(err.message, {
resolver: { history: resolver.getHistory() },
value,
object,
this.logger.error(`${err.message}`, {
resolver: {
history: resolver.getHistory(),
},
value: value,
object: object,
});
throw new Error('invalid note');
}
@@ -138,11 +144,7 @@ export class ApNoteService {
this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
if (note.attributedTo == null) {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
@@ -162,49 +164,59 @@ export class ApNoteService {
}
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag);
const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
const limit = promiseLimit<DriveFile>(2);
const files = (await Promise.all(toArray(note.attachment).map(attach => (
limit(() => this.apImageService.resolveImage(actor, {
...attach,
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
}))
))));
// Noteがsensitiveなら添付もsensitiveにする
const limit = promiseLimit(2);
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
.filter(image => image != null)
: [];
// リプライ
const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver)
.then(x => {
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
if (x == null) {
this.logger.warn('Specified inReplyTo, but not found');
throw new Error('inReplyTo not found');
}
} else {
return x;
})
.catch(async err => {
}
}).catch(async err => {
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
throw err;
})
: null;
// 引用
let quote: Note | undefined | null = null;
let quote: Note | undefined | null;
if (note._misskey_quote || note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise<
| { status: 'ok'; res: Note }
| { status: 'permerror' | 'temperror' }
> => {
if (!uri.match(/^https?:/)) return { status: 'permerror' };
const tryResolveNote = async (uri: string): Promise<{
status: 'ok';
res: Note | null;
} | {
status: 'permerror' | 'temperror';
}> => {
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
try {
const res = await this.resolveNote(uri);
if (res == null) return { status: 'permerror' };
return { status: 'ok', res };
if (res) {
return {
status: 'ok',
res,
};
} else {
return {
status: 'permerror',
};
}
} catch (e) {
return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
@@ -213,9 +225,9 @@ export class ApNoteService {
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(tryResolveNote));
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0);
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
if (!quote) {
if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed');
@@ -259,7 +271,7 @@ export class ApNoteService {
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
return [];
return [] as Emoji[];
});
const apEmojis = emojis.map(emoji => emoji.name);
@@ -297,18 +309,19 @@ export class ApNoteService {
const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断
// ブロックしてたら中断
const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
throw new StatusError('blocked host', 451);
}
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
const unlock = await this.appLockService.getApLock(uri);
try {
//#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion
if (uri.startsWith(this.config.url)) {
@@ -326,41 +339,43 @@ export class ApNoteService {
@bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
// eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host);
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({
host,
name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))),
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
});
return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name.replaceAll(':', '');
const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name);
if (exists) {
if ((exists.updatedAt == null)
if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null)
|| (new Date(tag.updated) > exists.updatedAt)
|| (tag.icon.url !== exists.originalUrl)
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|| (tag.icon!.url !== exists.originalUrl)
) {
await this.emojisRepository.update({
host,
name,
}, {
uri: tag.id,
originalUrl: tag.icon.url,
publicUrl: tag.icon.url,
originalUrl: tag.icon!.url,
publicUrl: tag.icon!.url,
updatedAt: new Date(),
});
const emoji = await this.emojisRepository.findOneBy({ host, name });
if (emoji == null) throw new Error('emoji update failed');
return emoji;
return await this.emojisRepository.findOneBy({
host,
name,
}) as Emoji;
}
return exists;
@@ -373,11 +388,11 @@ export class ApNoteService {
host,
name,
uri: tag.id,
originalUrl: tag.icon.url,
publicUrl: tag.icon.url,
originalUrl: tag.icon!.url,
publicUrl: tag.icon!.url,
updatedAt: new Date(),
aliases: [],
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
} as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
}));
}
}

View File

@@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
@@ -15,6 +15,7 @@ import type Logger from '@/logger.js';
import type { Note } from '@/models/entities/Note.js';
import type { IdService } from '@/core/IdService.js';
import type { MfmService } from '@/core/MfmService.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import { toArray } from '@/misc/prelude/array.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -47,8 +48,6 @@ import type { IActor, IObject } from '../type.js';
const nameLength = 128;
const summaryLength = 2048;
type Field = Record<'name' | 'value', string>;
@Injectable()
export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService;
@@ -95,10 +94,28 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
//private utilityService: UtilityService,
//private userEntityService: UserEntityService,
//private idService: IdService,
//private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private cacheService: CacheService,
//private apResolverService: ApResolverService,
//private apNoteService: ApNoteService,
//private apImageService: ApImageService,
//private apMfmService: ApMfmService,
//private mfmService: MfmService,
//private hashtagService: HashtagService,
//private usersChart: UsersChart,
//private instanceChart: InstanceChart,
//private apLoggerService: ApLoggerService,
) {
}
onModuleInit(): void {
onModuleInit() {
this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
@@ -136,6 +153,10 @@ export class ApPersonService implements OnModuleInit {
private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri);
if (x == null) {
throw new Error('invalid Actor: object is null');
}
if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`);
}
@@ -197,19 +218,21 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined;
if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null;
const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
if (u) this.cacheService.uriPersonCache.set(uri, u);
return u;
}
//#region このサーバーに既に登録されていたらそれを返す
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
if (exist) {
this.cacheService.uriPersonCache.set(uri, exist);
@@ -231,11 +254,9 @@ export class ApPersonService implements OnModuleInit {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
}
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
const object = await resolver.resolve(uri) as any;
const person = this.validateActor(object, uri);
@@ -243,9 +264,9 @@ export class ApPersonService implements OnModuleInit {
const host = this.punyHost(object.id);
const fields = this.analyzeAttachments(person.attachment ?? []);
const { fields } = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const isBot = getApType(object) === 'Service';
@@ -258,7 +279,7 @@ export class ApPersonService implements OnModuleInit {
}
// Create user
let user: RemoteUser | null = null;
let user: RemoteUser;
try {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
@@ -269,16 +290,16 @@ export class ApPersonService implements OnModuleInit {
createdAt: new Date(),
lastFetchedAt: new Date(),
name: truncate(person.name, nameLength),
isLocked: person.manuallyApprovesFollowers,
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs,
isExplorable: person.discoverable,
isExplorable: !!person.discoverable,
username: person.preferredUsername,
usernameLower: person.preferredUsername?.toLowerCase(),
usernameLower: person.preferredUsername!.toLowerCase(),
host,
inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id,
@@ -290,9 +311,9 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url,
url: url,
fields,
birthday: bday?.[0] ?? null,
birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null,
userHost: host,
}));
@@ -309,18 +330,21 @@ export class ApPersonService implements OnModuleInit {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await this.usersRepository.findOneBy({ uri: person.id });
if (u == null) throw new Error('already registered');
const u = await this.usersRepository.findOneBy({
uri: person.id,
});
if (u) {
user = u as RemoteUser;
} else {
throw new Error('already registered');
}
} else {
this.logger.error(e instanceof Error ? e : new Error(e as string));
throw e;
}
}
if (user == null) throw new Error('failed to create user: user is null');
// Register host
this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
@@ -330,26 +354,29 @@ export class ApPersonService implements OnModuleInit {
}
});
this.usersChart.update(user, true);
this.usersChart.update(user!, true);
// ハッシュタグ更新
this.hashtagService.updateUsertags(user, tags);
this.hashtagService.updateUsertags(user!, tags);
//#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
if (img == null) return null;
if (user == null) throw new Error('failed to create user: user is null');
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
const [avatar, banner] = await Promise.all([
person.icon,
person.image,
].map(img =>
img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(user!, img).catch(() => null),
));
const avatarId = avatar?.id ?? null;
const bannerId = banner?.id ?? null;
const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar?.blurhash ?? null;
const bannerBlurhash = banner?.blurhash ?? null;
const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user.id, {
await this.usersRepository.update(user!.id, {
avatarId,
bannerId,
avatarUrl,
@@ -358,28 +385,30 @@ export class ApPersonService implements OnModuleInit {
bannerBlurhash,
});
user.avatarId = avatarId;
user.bannerId = bannerId;
user.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl;
user.avatarBlurhash = avatarBlurhash;
user.bannerBlurhash = bannerBlurhash;
user!.avatarId = avatarId;
user!.bannerId = bannerId;
user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [];
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user.id, { emojis: emojiNames });
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user;
return user!;
}
/**
@@ -397,14 +426,18 @@ export class ApPersonService implements OnModuleInit {
if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(`${this.config.url}/`)) return;
if (uri.startsWith(`${this.config.url}/`)) {
return;
}
//#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
if (exist === null) return;
if (exist === null) {
return;
}
//#endregion
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri);
@@ -414,22 +447,26 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
if (img == null) return null;
return this.apImageService.resolveImage(exist, img).catch(() => null);
}));
const [avatar, banner] = await Promise.all([
person.icon,
person.image,
].map(img =>
img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(exist, img).catch(() => null),
));
// カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
return [];
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
const fields = this.analyzeAttachments(person.attachment ?? []);
const { fields } = this.analyzeAttachments(person.attachment ?? []);
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -442,7 +479,7 @@ export class ApPersonService implements OnModuleInit {
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured,
emojis: emojiNames,
@@ -450,29 +487,18 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable,
isExplorable: !!person.discoverable,
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => {
const moving =
// 移行先がない→ある
if (
exist.movedToUri === null &&
updates.movedToUri
) return true;
(!exist.movedToUri && updates.movedToUri) ||
// 移行先がある→別のもの
if (
exist.movedToUri !== null &&
updates.movedToUri !== null &&
exist.movedToUri !== updates.movedToUri
) return true;
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
// 移行先がある→ない、ない→ないは無視
return false;
})();
if (moving) updates.movedAt = new Date();
@@ -499,10 +525,10 @@ export class ApPersonService implements OnModuleInit {
}
await this.userProfilesRepository.update({ userId: exist.id }, {
url,
url: url,
fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday?.[0] ?? null,
birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null,
});
@@ -512,10 +538,11 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await this.followingsRepository.update(
{ followerId: exist.id },
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
);
await this.followingsRepository.update({
followerId: exist.id,
}, {
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
});
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
@@ -553,22 +580,27 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion
// リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
return await this.createPerson(uri, resolver);
}
@bindThis
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
const fields: Field[] = [];
public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
const fields: {
name: string,
value: string
}[] = [];
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
fields.push({
@@ -578,11 +610,11 @@ export class ApPersonService implements OnModuleInit {
}
}
return fields;
return { fields };
}
@bindThis
public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> {
public async updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return;
@@ -611,13 +643,13 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持
let td = 0;
for (const note of featuredNotes.filter((note): note is Note => note != null)) {
for (const note of featuredNotes.filter(note => note != null)) {
td -= 1000;
transactionalEntityManager.insert(UserNotePining, {
id: this.idService.genId(new Date(Date.now() + td)),
createdAt: new Date(),
userId: user.id,
noteId: note.id,
noteId: note!.id,
});
}
});

View File

@@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { IPoll } from '@/models/entities/Poll.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IQuestion } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApQuestionService {
@@ -33,25 +33,33 @@ export class ApQuestionService {
@bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
const multiple = question.oneOf === undefined;
if (multiple && question.anyOf === undefined) throw new Error('invalid question');
if (!isQuestion(question)) {
throw new Error('invalid type');
}
const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name)
.filter((x): x is string => typeof x === 'string')
?? [];
if (multiple && !question.anyOf) {
throw new Error('invalid question');
}
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
const choices = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.name!);
return { choices, votes, multiple, expiresAt };
const votes = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return {
choices,
votes,
multiple,
expiresAt,
};
}
/**
@@ -60,9 +68,8 @@ export class ApQuestionService {
* @returns true if updated
*/
@bindThis
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
public async updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('uri is null');
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
@@ -76,7 +83,6 @@ export class ApQuestionService {
//#endregion
// resolve new Question object
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const question = await resolver.resolve(value) as IQuestion;
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
@@ -84,14 +90,12 @@ export class ApQuestionService {
if (question.type !== 'Question') throw new Error('object is not a Question');
const apChoices = question.oneOf ?? question.anyOf;
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
let changed = false;
for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (oldCount !== newCount) {
changed = true;
@@ -99,7 +103,9 @@ export class ApQuestionService {
}
}
await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes });
await this.pollsRepository.update({ noteId: note.id }, {
votes: poll.votes,
});
return changed;
}

View File

@@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js';
export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] {
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return [];
const hashtags = extractApHashtagObjects(tags);

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@@ -31,7 +31,6 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
Packed<'UserDetailed'> :
Packed<'UserLite'>;
const Ajv = _Ajv.default;
const ajv = new Ajv();
function isLocalUser(user: User): user is LocalUser;

View File

@@ -3,7 +3,6 @@ import si from 'systeminformation';
import Xev from 'xev';
import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev();
@@ -15,10 +14,9 @@ const round = (num: number) => Math.round(num * 10) / 10;
@Injectable()
export class ServerStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer | null = null;
private intervalId: NodeJS.Timer;
constructor(
private metaService: MetaService,
) {
}
@@ -26,9 +24,7 @@ export class ServerStatsService implements OnApplicationShutdown {
* Report server stats regularly
*/
@bindThis
public async start(): Promise<void> {
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
public start(): void {
const log = [] as any[];
ev.on('requestServerStatsLog', x => {
@@ -68,10 +64,8 @@ export class ServerStatsService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {

View File

@@ -4,7 +4,7 @@ import { default as convertColor } from 'color-convert';
import { format as dateFormat } from 'date-fns';
import { bindThis } from '@/decorators.js';
import { envOption } from './env.js';
import type { KEYWORD } from 'color-convert/conversions.js';
import type { KEYWORD } from 'color-convert/conversions';
type Context = {
name: string;

View File

@@ -83,16 +83,6 @@ export class RedisKVCache<T> {
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
@bindThis
public gc() {
this.memoryCache.gc();
}
@bindThis
public dispose() {
this.memoryCache.dispose();
}
}
export class RedisSingleCache<T> {
@@ -184,15 +174,10 @@ export class RedisSingleCache<T> {
export class MemoryKVCache<T> {
public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
private gcIntervalHandle: NodeJS.Timer;
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
this.gcIntervalHandle = setInterval(() => {
this.gc();
}, 1000 * 60 * 3);
}
@bindThis
@@ -215,7 +200,7 @@ export class MemoryKVCache<T> {
}
@bindThis
public delete(key: string): void {
public delete(key: string) {
this.cache.delete(key);
}
@@ -270,21 +255,6 @@ export class MemoryKVCache<T> {
}
return value;
}
@bindThis
public gc(): void {
const now = Date.now();
for (const [key, { date }] of this.cache.entries()) {
if ((now - date) > this.lifetime) {
this.cache.delete(key);
}
}
}
@bindThis
public dispose(): void {
clearInterval(this.gcIntervalHandle);
}
}
export class MemorySingleCache<T> {

View File

@@ -1,4 +1,4 @@
export function checkHttps(url: string): boolean {
export function checkHttps(url: string) {
return url.startsWith('https://') ||
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
}

View File

@@ -1,3 +1,3 @@
import { secureRndstr } from '@/misc/secure-rndstr.js';
export default () => secureRndstr(16);
export default () => secureRndstr(16, true);

View File

@@ -1,6 +1,6 @@
import IPCIDR from 'ip-cidr';
export function getIpHash(ip: string): string {
export function getIpHash(ip: string) {
try {
// because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account.

View File

@@ -1,9 +1,10 @@
import * as crypto from 'node:crypto';
export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string {
export function secureRndstr(length = 32, useLU = true): string {
const chars = useLU ? LU_CHARS : L_CHARS;
const chars_len = chars.length;
let str = '';

View File

@@ -55,10 +55,7 @@ export class Ad {
length: 8192, nullable: false,
})
public memo: string;
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
constructor(data: Partial<Ad>) {
if (data == null) return;

View File

@@ -97,30 +97,18 @@ export class Meta {
})
public logoImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public errorImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public iconUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public serverErrorImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public notFoundImageUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public infoImageUrl: string | null;
@Column('boolean', {
default: true,
})
@@ -413,16 +401,6 @@ export class Meta {
})
public enableChartsForFederatedInstances: boolean;
@Column('boolean', {
default: false,
})
public enableServerMachineStats: boolean;
@Column('boolean', {
default: true,
})
public enableIdenticonGeneration: boolean;
@Column('jsonb', {
default: { },
})

View File

@@ -1,5 +1,5 @@
import { Injectable, Inject } from '@nestjs/common';
import _Ajv from 'ajv';
import Ajv from 'ajv';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import Logger from '@/logger.js';
@@ -10,8 +10,6 @@ import { QueueLoggerService } from '../QueueLoggerService.js';
import { DBAntennaImportJobData } from '../types.js';
import type * as Bull from 'bullmq';
const Ajv = _Ajv.default;
const validate = new Ajv().compile({
type: 'object',
properties: {

View File

@@ -369,7 +369,7 @@ export class ActivityPubServerService {
}))
.andWhere('note.localOnly = FALSE');
const notes = await query.limit(limit).getMany();
const notes = await query.take(limit).getMany();
if (sinceId) notes.reverse();

View File

@@ -16,7 +16,6 @@ import { createTemp } from '@/misc/create-temp.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@@ -46,7 +45,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private metaService: MetaService,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
@@ -163,16 +161,11 @@ export class ServerService implements OnApplicationShutdown {
});
fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => {
reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
const [temp, cleanup] = await createTemp();
await genIdenticon(request.params.x, fs.createWriteStream(temp));
reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
return fs.createReadStream(temp).on('close', () => cleanup());
} else {
return reply.redirect('/static-assets/avatar.png');
}
});
fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {

View File

@@ -53,30 +53,37 @@ export class ApiCallService implements OnApplicationShutdown {
}, 1000 * 60 * 60);
}
#sendApiError(reply: FastifyReply, err: ApiError): void {
let statusCode = err.httpStatusCode;
if (err.httpStatusCode === 401) {
reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
} else if (err.kind === 'permission') {
// (ROLE_PERMISSION_DENIEDは関係ない)
if (err.code === 'PERMISSION_DENIED') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
} else if (!statusCode) {
statusCode = 500;
}
this.send(reply, statusCode, err);
}
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
reply: FastifyReply,
) {
const body = request.method === 'GET'
? request.query
: request.body;
#sendAuthenticationError(reply: FastifyReply, err: unknown): void {
const token = body?.['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, body, null, request).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
this.send(reply, res);
}).catch((err: ApiError) => {
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
});
if (user) {
this.logIp(request, user);
}
}).catch(err => {
if (err instanceof AuthenticationError) {
const message = 'Authentication failed. Please ensure your token is correct.';
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_token", error_description="${message}"`);
this.send(reply, 401, new ApiError({
this.send(reply, 403, new ApiError({
message: 'Authentication failed. Please ensure your token is correct.',
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
@@ -84,41 +91,6 @@ export class ApiCallService implements OnApplicationShutdown {
} else {
this.send(reply, 500, new ApiError());
}
}
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
reply: FastifyReply,
): void {
const body = request.method === 'GET'
? request.query
: request.body;
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
const token = request.headers.authorization?.startsWith('Bearer ')
? request.headers.authorization.slice(7)
: body?.['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, body, null, request).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
});
if (user) {
this.logIp(request, user);
}
}).catch(err => {
this.#sendAuthenticationError(reply, err);
});
}
@@ -127,7 +99,7 @@ export class ApiCallService implements OnApplicationShutdown {
endpoint: IEndpoint & { exec: any },
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
reply: FastifyReply,
): Promise<void> {
) {
const multipartData = await request.file().catch(() => {
/* Fastify throws if the remote didn't send multipart data. Return 400 below. */
});
@@ -145,10 +117,7 @@ export class ApiCallService implements OnApplicationShutdown {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
}
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
const token = request.headers.authorization?.startsWith('Bearer ')
? request.headers.authorization.slice(7)
: fields['i'];
const token = fields['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -160,14 +129,22 @@ export class ApiCallService implements OnApplicationShutdown {
}, request).then((res) => {
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
});
if (user) {
this.logIp(request, user);
}
}).catch(err => {
this.#sendAuthenticationError(reply, err);
if (err instanceof AuthenticationError) {
this.send(reply, 403, new ApiError({
message: 'Authentication failed. Please ensure your token is correct.',
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
}));
} else {
this.send(reply, 500, new ApiError());
}
});
}
@@ -278,8 +255,8 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'Your account has been suspended.',
code: 'YOUR_ACCOUNT_SUSPENDED',
kind: 'permission',
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
httpStatusCode: 403,
});
}
}
@@ -289,8 +266,8 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'You have moved your account.',
code: 'YOUR_ACCOUNT_MOVED',
kind: 'permission',
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
httpStatusCode: 403,
});
}
}
@@ -301,7 +278,6 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'You are not assigned to a moderator role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
});
}
@@ -309,7 +285,6 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'You are not assigned to an administrator role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
});
}
@@ -321,7 +296,6 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
});
}
@@ -331,7 +305,6 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
kind: 'permission',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
});
}

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
import type { LocalUser } from '@/models/entities/User.js';
@@ -17,7 +17,7 @@ export class AuthenticationError extends Error {
}
@Injectable()
export class AuthenticateService implements OnApplicationShutdown {
export class AuthenticateService {
private appCache: MemoryKVCache<App>;
constructor(
@@ -85,14 +85,4 @@ export class AuthenticateService implements OnApplicationShutdown {
}
}
}
@bindThis
public dispose(): void {
this.appCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -333,6 +333,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
@@ -673,6 +674,7 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas
const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
@@ -1017,6 +1019,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_searchByUsernameAndHost,
$users_search,
$users_show,
$users_stats,
$users_achievements,
$users_updateMemo,
$fetchRss,
@@ -1353,6 +1356,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_searchByUsernameAndHost,
$users_search,
$users_show,
$users_stats,
$users_achievements,
$users_updateMemo,
$fetchRss,

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import rndstr from 'rndstr';
import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
@@ -15,7 +16,6 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
@Injectable()
export class SignupApiService {
@@ -142,7 +142,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'DENIED_USERNAME');
}
const code = secureRndstr(16, { chars: L_CHARS });
const code = rndstr('a-z0-9', 16);
// Generate hash of password
const salt = await bcrypt.genSalt(8);

View File

@@ -10,7 +10,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { LocalUser } from '@/models/entities/User.js';
import { LocalUser } from '@/models/entities/User';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/index.js';
import { ChannelsService } from './stream/ChannelsService.js';
@@ -58,21 +58,11 @@ export class StreamingApiServerService {
let user: LocalUser | null = null;
let app: AccessToken | null = null;
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1
// Note that the standard WHATWG WebSocket API does not support setting any headers,
// but non-browser apps may still be able to set it.
const token = request.headers.authorization?.startsWith('Bearer ')
? request.headers.authorization.slice(7)
: q.get('i');
try {
[user, app] = await this.authenticateService.authenticate(token);
[user, app] = await this.authenticateService.authenticate(q.get('i'));
} catch (e) {
if (e instanceof AuthenticationError) {
socket.write([
'HTTP/1.1 401 Unauthorized',
'WWW-Authenticate: Bearer realm="Misskey", error="invalid_token", error_description="Failed to authenticate"',
].join('\r\n') + '\r\n\r\n');
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
} else {
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
}
@@ -103,13 +93,6 @@ export class StreamingApiServerService {
});
});
const globalEv = new EventEmitter();
this.redisForSub.on('message', (_: string, data: string) => {
const parsed = JSON.parse(data);
globalEv.emit('message', parsed);
});
this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
stream: MainStreamConnection,
user: LocalUser | null;
@@ -119,11 +102,12 @@ export class StreamingApiServerService {
const ev = new EventEmitter();
function onRedisMessage(data: any): void {
ev.emit(data.channel, data.message);
async function onRedisMessage(_: string, data: string): Promise<void> {
const parsed = JSON.parse(data);
ev.emit(parsed.channel, parsed.message);
}
globalEv.on('message', onRedisMessage);
this.redisForSub.on('message', onRedisMessage);
await stream.listen(ev, connection);
@@ -143,28 +127,27 @@ export class StreamingApiServerService {
connection.once('close', () => {
ev.removeAllListeners();
stream.dispose();
globalEv.off('message', onRedisMessage);
this.#connections.delete(connection);
this.redisForSub.off('message', onRedisMessage);
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
});
connection.on('pong', () => {
connection.on('message', async (data) => {
this.#connections.set(connection, Date.now());
if (data.toString() === 'ping') {
connection.send('pong');
}
});
});
// 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする
this.#cleanConnectionsIntervalId = setInterval(() => {
const now = Date.now();
for (const [connection, lastActive] of this.#connections.entries()) {
if (now - lastActive > 1000 * 60 * 2) {
if (now - lastActive > 1000 * 60 * 5) {
connection.terminate();
this.#connections.delete(connection);
} else {
connection.ping();
}
}
}, 1000 * 60);
}, 1000 * 60 * 5);
}
@bindThis

View File

@@ -1,13 +1,11 @@
import * as fs from 'node:fs';
import _Ajv from 'ajv';
import Ajv from 'ajv';
import type { Schema, SchemaType } from '@/misc/json-schema.js';
import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import { ApiError } from './error.js';
import type { IEndpointMeta } from './endpoints.js';
const Ajv = _Ajv.default;
const ajv = new Ajv({
useDefaults: true,
});

View File

@@ -333,6 +333,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
@@ -671,6 +672,7 @@ const eps = [
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/stats', ep___users_stats],
['users/achievements', ep___users_achievements],
['users/update-memo', ep___users_updateMemo],
['fetch-rss', ep___fetchRss],

View File

@@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break;
}
const reports = await query.limit(ps.limit).getMany();
const reports = await query.take(ps.limit).getMany();
return await this.abuseUserReportEntityService.packMany(reports);
});

View File

@@ -22,9 +22,8 @@ export const paramDef = {
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -42,7 +41,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,

View File

@@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
const ads = await query.limit(ps.limit).getMany();
const ads = await query.take(ps.limit).getMany();
return ads;
});

View File

@@ -31,9 +31,8 @@ export const paramDef = {
ratio: { type: 'integer' },
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
},
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -57,7 +56,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
});
});
}

View File

@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const announcements = await query.limit(ps.limit).getMany();
const announcements = await query.take(ps.limit).getMany();
const reads = new Map<Announcement, number>();

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
}
const files = await query.limit(ps.limit).getMany();
const files = await query.take(ps.limit).getMany();
return await this.driveFileEntityService.packMany(files, { detail: true, withUser: true, self: true });
});

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import rndstr from 'rndstr';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';

View File

@@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const emojis = await q
.orderBy('emoji.id', 'DESC')
.limit(ps.limit)
.take(ps.limit)
.getMany();
return this.emojiEntityService.packDetailedMany(emojis);

View File

@@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.query) {
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
//const emojis = await q.limit(ps.limit).getMany();
//const emojis = await q.take(ps.limit).getMany();
emojis = await q.getMany();
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
emojis.splice(ps.limit + 1);
} else {
emojis = await q.limit(ps.limit).getMany();
emojis = await q.take(ps.limit).getMany();
}
return this.emojiEntityService.packDetailedMany(emojis);

View File

@@ -61,17 +61,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
errorImageUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://xn--931a.moe/aiart/yubitun.png',
},
iconUrl: {
type: 'string',
@@ -262,14 +255,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
enableServerMachineStats: {
type: 'boolean',
optional: false, nullable: false,
},
enableIdenticonGeneration: {
type: 'boolean',
optional: false, nullable: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
@@ -320,9 +305,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
infoImageUrl: instance.infoImageUrl,
errorImageUrl: instance.errorImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
@@ -372,8 +355,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
};
});

View File

@@ -33,17 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
delayedQueues = await this.queueService.deliverQueue.getDelayed();
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
const queue = delayedQueues[queueIndex];
try {
await queue.promote();
} catch (e) {
if (e instanceof Error) {
if (e.message.indexOf('not in a delayed state') !== -1) {
throw e;
}
} else {
throw e;
}
}
}
break;
@@ -51,17 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
delayedQueues = await this.queueService.inboxQueue.getDelayed();
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
const queue = delayedQueues[queueIndex];
try {
await queue.promote();
} catch (e) {
if (e instanceof Error) {
if (e.message.indexOf('not in a delayed state') !== -1) {
throw e;
}
} else {
throw e;
}
}
}
break;
}

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import rndstr from 'rndstr';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
export const meta = {
tags: ['admin'],
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('cannot reset password of root');
}
const passwd = secureRndstr(8);
const passwd = rndstr('a-zA-Z0-9', 8);
// Generate hash of password
const hash = bcrypt.hashSync(passwd);

View File

@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.innerJoinAndSelect('assign.user', 'user');
const assigns = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await Promise.all(assigns.map(async assign => ({

View File

@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
const reports = await query.limit(ps.limit).getMany();
const reports = await query.take(ps.limit).getMany();
return await this.moderationLogEntityService.packMany(reports);
});

View File

@@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
default: query.orderBy('user.id', 'ASC'); break;
}
query.limit(ps.limit);
query.take(ps.limit);
query.skip(ps.offset);
const users = await query.getMany();

View File

@@ -32,9 +32,7 @@ export const paramDef = {
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
serverErrorImageUrl: { type: 'string', nullable: true },
infoImageUrl: { type: 'string', nullable: true },
notFoundImageUrl: { type: 'string', nullable: true },
errorImageUrl: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true },
@@ -96,8 +94,6 @@ export const paramDef = {
enableActiveEmailValidation: { type: 'boolean' },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } },
},
@@ -153,18 +149,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.iconUrl = ps.iconUrl;
}
if (ps.serverErrorImageUrl !== undefined) {
set.serverErrorImageUrl = ps.serverErrorImageUrl;
}
if (ps.infoImageUrl !== undefined) {
set.infoImageUrl = ps.infoImageUrl;
}
if (ps.notFoundImageUrl !== undefined) {
set.notFoundImageUrl = ps.notFoundImageUrl;
}
if (ps.backgroundImageUrl !== undefined) {
set.backgroundImageUrl = ps.backgroundImageUrl;
}
@@ -297,6 +281,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.smtpPass = ps.smtpPass;
}
if (ps.errorImageUrl !== undefined) {
set.errorImageUrl = ps.errorImageUrl;
}
if (ps.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker;
}
@@ -401,14 +389,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
if (ps.enableServerMachineStats !== undefined) {
set.enableServerMachineStats = ps.enableServerMachineStats;
}
if (ps.enableIdenticonGeneration !== undefined) {
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
}
if (ps.serverRules !== undefined) {
set.serverRules = ps.serverRules;
}

View File

@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const announcements = await query.limit(ps.limit).getMany();
const announcements = await query.take(ps.limit).getMany();
if (me) {
const reads = (await this.announcementReadsRepository.findBy({

View File

@@ -44,7 +44,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
// Generate secret
const secret = secureRndstr(32);
const secret = secureRndstr(32, true);
// for backward compatibility
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));

View File

@@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchSession);
}
const accessToken = secureRndstr(32);
const accessToken = secureRndstr(32, true);
// Fetch exist access token
const exist = await this.accessTokensRepository.findOneBy({

View File

@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('blocking.blockerId = :meId', { meId: me.id });
const blockings = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await this.blockingEntityService.packMany(blockings, me);

View File

@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('channel.isArchived = FALSE')
.orderBy('channel.lastNotedAt', 'DESC');
const channels = await query.limit(10).getMany();
const channels = await query.take(10).getMany();
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));
});

View File

@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere({ followerId: me.id });
const followings = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me)));

View File

@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere({ userId: me.id });
const channels = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));

View File

@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
const channels = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));

View File

@@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
timeline = await query.limit(ps.limit).getMany();
timeline = await query.take(ps.limit).getMany();
} else {
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);

View File

@@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
const notes = await query
.limit(ps.limit)
.take(ps.limit)
.getMany();
return await this.noteEntityService.packMany(notes, me);

View File

@@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
case '-size': query.orderBy('file.size', 'ASC'); break;
}
const files = await query.limit(ps.limit).getMany();
const files = await query.take(ps.limit).getMany();
return await this.driveFileEntityService.packMany(files, { detail: false, self: true });
});

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