Compare commits

..

82 Commits

Author SHA1 Message Date
Kagami Sascha Rosylight
8aa350ced4 Update api.ts 2023-06-28 23:28:43 +02:00
Kagami Sascha Rosylight
93364cb922 update tests with updated util function 2023-06-28 23:17:58 +02:00
Kagami Sascha Rosylight
1f38d624c0 send WWW-Authenticate where it's possible 2023-06-28 22:16:31 +02:00
Kagami Sascha Rosylight
deb9ba146f Update oauth.vue 2023-06-28 22:16:07 +02:00
Kagami Sascha Rosylight
833df85457 UserToken 2023-06-28 22:16:07 +02:00
Kagami Sascha Rosylight
d340860b8b import order 2023-06-28 22:16:07 +02:00
Kagami Sascha Rosylight
d1534ec64e www-authenticate 2023-06-28 22:16:06 +02:00
Kagami Sascha Rosylight
16a73dea26 Update oauth.pug 2023-06-28 22:15:41 +02:00
Kagami Sascha Rosylight
d0d9b4b19c remove redundant dependencies 2023-06-28 22:15:41 +02:00
Kagami Sascha Rosylight
ca7c3c6063 remove redundant function calls 2023-06-28 22:15:41 +02:00
Kagami Sascha Rosylight
cb2089981a quote 2023-06-28 22:15:40 +02:00
Kagami Sascha Rosylight
daa18efc99 generate the code later 2023-06-28 22:15:40 +02:00
Kagami Sascha Rosylight
0b3fd09bb0 no token expiration? 2023-06-28 22:15:40 +02:00
Kagami Sascha Rosylight
1567a2ea3e error in rfc6750 2023-06-28 22:15:40 +02:00
Kagami Sascha Rosylight
ecdd1c115a Revoke access token if the code is reused 2023-06-28 22:15:29 +02:00
Kagami Sascha Rosylight
d7e0e9feca todo: revoke all tokens 2023-06-28 22:15:29 +02:00
Kagami Sascha Rosylight
7ed8fbbba3 GetTokenError 2023-06-28 22:15:29 +02:00
Kagami Sascha Rosylight
5db1126db6 clientConfig 2023-06-28 22:15:29 +02:00
Kagami Sascha Rosylight
628377187a grant type tests 2023-06-28 22:15:29 +02:00
Kagami Sascha Rosylight
b57d40ed09 typo 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
1755c75647 some edits for comments 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
c55d9784fe migration todo 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
52e7bdd817 import changes 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
260ac0ecfc solve typescript warnings 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
b81e6eeff9 rfc 8252 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
15f859d562 Return 403 from permission error 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
b938bc7c52 more description about client id validation 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
20efdc78e2 add more comments 2023-06-28 22:15:28 +02:00
Kagami Sascha Rosylight
aa87fb2f50 merge wildcard binder to createServer 2023-06-28 22:15:06 +02:00
Kagami Sascha Rosylight
95dd66a0ba more assertions for indirect errors 2023-06-28 22:15:06 +02:00
Kagami Sascha Rosylight
c83628e5d0 use logger 2023-06-28 22:15:06 +02:00
Kagami Sascha Rosylight
d0245b59bc add another error handler for non-indirect case 2023-06-28 22:15:06 +02:00
Kagami Sascha Rosylight
4c12a9d882 fix typo 2023-06-28 22:15:05 +02:00
Kagami Sascha Rosylight
d245306d90 helpers for error assertions 2023-06-28 22:15:05 +02:00
Kagami Sascha Rosylight
0d2041f5aa mode: indirect 2023-06-28 22:15:05 +02:00
Kagami Sascha Rosylight
b5df8ca0fd 404 test 2023-06-28 22:15:05 +02:00
Kagami Sascha Rosylight
3b8b9a658a Add authorization code tests 2023-06-28 22:15:05 +02:00
Kagami Sascha Rosylight
413fa63093 remove needless as any 2023-06-28 22:15:04 +02:00
Kagami Sascha Rosylight
347a4a0b93 Decision endpoint tests 2023-06-28 22:15:04 +02:00
Kagami Sascha Rosylight
bfe6e5abb8 remove confusing return [false]; 2023-06-28 22:15:04 +02:00
Kagami Sascha Rosylight
78c6bb1cc2 dedupe CID test logic 2023-06-28 22:15:04 +02:00
Kagami Sascha Rosylight
9a5fa00f9a reduce typescript warnings on tests 2023-06-28 22:15:04 +02:00
Kagami Sascha Rosylight
967989c5f8 dedupe test logic 2023-06-28 22:15:03 +02:00
Kagami Sascha Rosylight
c25836bc1a Split PKCE verification test 2023-06-28 22:15:03 +02:00
Kagami Sascha Rosylight
9022971fb9 precomputed pkce test 2023-06-28 22:15:03 +02:00
Kagami Sascha Rosylight
cb5cfd4296 remove express-session 2023-06-28 22:15:03 +02:00
Kagami Sascha Rosylight
cbaae2201f use MemoryKVCache for oauth store 2023-06-28 22:15:03 +02:00
Kagami Sascha Rosylight
2c6379649a Update OAuth2ProviderService.ts 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
150a6f80d0 Use MemoryKVCache 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
c0f63234d7 use verifyChallenge 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
9c29880f8b Update to @types/oauth2orize@1.11, fix type errors 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
2b23120664 upgrade to pkce-challenge@4 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
b6f6819b76 todo 2023-06-28 22:15:02 +02:00
Kagami Sascha Rosylight
77ad8c0ac6 reduce type errors with pkce params 2023-06-28 22:15:01 +02:00
Kagami Sascha Rosylight
92f3ae2d9c reduce any using OAuthErrorResponse 2023-06-28 22:15:01 +02:00
Kagami Sascha Rosylight
94ea15d2d7 merge authorization validation logic 2023-06-28 22:15:01 +02:00
Kagami Sascha Rosylight
8e7fc1ed98 use errorHandler() 2023-06-28 22:15:01 +02:00
Kagami Sascha Rosylight
937e9be34e fix import order 2023-06-28 22:15:01 +02:00
Kagami Sascha Rosylight
027c5734a4 concurrent flow test 2023-06-28 22:15:00 +02:00
Kagami Sascha Rosylight
a688bd1061 more discovery test 2023-06-28 22:15:00 +02:00
Kagami Sascha Rosylight
87dbe5e9fb client info discovery test 2023-06-28 22:15:00 +02:00
Kagami Sascha Rosylight
f6d9cf1ef1 strict redirection uri 2023-06-28 22:15:00 +02:00
Kagami Sascha Rosylight
333d6a9283 server metadata test 2023-06-28 22:15:00 +02:00
Kagami Sascha Rosylight
deb4429e3a return scope in token response 2023-06-28 22:14:59 +02:00
Kagami Sascha Rosylight
6385ca9b0d iss parameter test 2023-06-28 22:14:59 +02:00
Kagami Sascha Rosylight
515af3176a redirection test 2023-06-28 22:14:59 +02:00
Kagami Sascha Rosylight
0cc9d5aa32 header test 2023-06-28 22:14:59 +02:00
Kagami Sascha Rosylight
401575a903 scope test 2023-06-28 22:14:59 +02:00
Kagami Sascha Rosylight
88fd7f2758 test comment 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
5034e6cd69 PKCE verification test 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
2f566e4173 resolve conflicts 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
179640af30 todos 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
098d0670a3 a bit more tests 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
71f62b9d89 tmp 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
82c9820ac8 tmp 2023-06-28 22:14:58 +02:00
Kagami Sascha Rosylight
39526d0225 tmp 2023-06-28 22:14:57 +02:00
Kagami Sascha Rosylight
049dbfeb66 tmp 2023-06-28 22:14:57 +02:00
Kagami Sascha Rosylight
8ea1288234 tmp 2023-06-28 22:14:35 +02:00
Kagami Sascha Rosylight
a55d3f7382 tmp 2023-06-28 22:14:35 +02:00
Kagami Sascha Rosylight
f5a6509663 tmp 2023-06-28 22:14:34 +02:00
Kagami Sascha Rosylight
a4fb17620c tmp 2023-06-28 22:14:34 +02:00
Kagami Sascha Rosylight
0621e94c7d tmp 2023-06-28 22:14:34 +02:00
315 changed files with 5159 additions and 4313 deletions

View File

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

View File

@@ -2,7 +2,7 @@ version: '3.8'
services: services:
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

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

View File

@@ -54,7 +54,7 @@ Please include errors from the developer console and/or server log files if you
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment --> * Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
* Misskey: 13.x.x * Misskey: 13.x.x
* Node: 20.x.x * Node: 18.x.x
* PostgreSQL: 15.x.x * PostgreSQL: 15.x.x
* Redis: 7.x.x * Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 --> * OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View File

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

View File

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

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

@@ -1 +1 @@
20.3.1 18.16.0

View File

@@ -14,27 +14,8 @@
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### General
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client ### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- Fix: サーバーメトリクスが90度傾いている - Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- 全体的なDBクエリのパフォーマンスを向上
## 13.13.2 ## 13.13.2

View File

@@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development ## Development
During development, it is useful to use the During development, it is useful to use the
``` ```
pnpm dev pnpm dev
@@ -150,7 +150,7 @@ Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/docker-compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test. Run all test.
``` ```

View File

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

View File

@@ -2,9 +2,9 @@
<a href="https://misskey-hub.net"> <a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/> <img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a> </a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
--- ---
<a href="https://misskey-hub.net/instances.html"> <a href="https://misskey-hub.net/instances.html">
@@ -21,7 +21,7 @@
<a href="https://www.patreon.com/syuilo"> <a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
--- ---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey) [![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)

View File

@@ -23,13 +23,13 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<style> <style>
#g8 { #g8 {
animation-name: floating; animation-name: floating;
animation-duration: 3s; animation-duration: 3s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
} }
@keyframes floating { @keyframes floating {
0% { transform: translate(0, 0px); } 0% { transform: translate(0, 0px); }
50% { transform: translate(0, -5px); } 50% { transform: translate(0, -5px); }

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -56,7 +56,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');

10
locales/index.d.ts vendored
View File

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

View File

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

View File

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

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,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", "main": "./index.js",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": ">=18.16.0"
},
"scripts": { "scripts": {
"start": "node ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
@@ -54,35 +51,36 @@
"utf-8-validate": "^6.0.3" "utf-8-validate": "^6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.367.0", "@aws-sdk/client-s3": "3.321.1",
"@aws-sdk/lib-storage": "3.367.0", "@aws-sdk/lib-storage": "3.321.1",
"@aws-sdk/node-http-handler": "3.360.0", "@aws-sdk/node-http-handler": "3.321.1",
"@bull-board/api": "5.6.0", "@bull-board/api": "5.5.3",
"@bull-board/fastify": "5.6.0", "@bull-board/fastify": "5.5.3",
"@bull-board/ui": "5.6.0", "@bull-board/ui": "5.5.3",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0", "@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0", "@fastify/cors": "8.3.0",
"@fastify/express": "^2.3.0",
"@fastify/http-proxy": "9.2.1", "@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.0", "@fastify/multipart": "7.7.0",
"@fastify/static": "6.10.2", "@fastify/static": "6.10.2",
"@fastify/view": "8.0.0", "@fastify/view": "7.4.1",
"@nestjs/common": "10.0.5", "@nestjs/common": "10.0.3",
"@nestjs/core": "10.0.5", "@nestjs/core": "10.0.3",
"@nestjs/testing": "10.0.5", "@nestjs/testing": "10.0.3",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0", "@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.68", "@swc/core": "1.3.66",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"async-mutex": "^0.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "4.2.0", "body-parser": "^1.20.2",
"bullmq": "4.1.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.0", "cbor": "9.0.0",
"chalk": "5.2.0", "chalk": "5.2.0",
@@ -94,18 +92,19 @@
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.19.2", "fastify": "4.18.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "13.0.0", "got": "13.0.0",
"happy-dom": "10.0.3", "happy-dom": "9.20.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"http-link-header": "^1.1.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0", "ipaddr.js": "2.1.0",
"is-svg": "5.0.0", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
@@ -121,10 +120,13 @@
"nodemailer": "6.9.3", "nodemailer": "6.9.3",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "^1.11.1",
"oauth2orize-pkce": "^0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.1.3", "otpauth": "9.1.2",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.1", "pg": "8.11.0",
"pkce-challenge": "^4.0.1",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
@@ -148,14 +150,14 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.6", "systeminformation": "5.18.4",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.17", "typeorm": "0.3.17",
"typescript": "5.1.6", "typescript": "5.1.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.14", "unzipper": "0.10.14",
"uuid": "9.0.0", "uuid": "9.0.0",
@@ -165,16 +167,18 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.6.1", "@jest/globals": "29.5.0",
"@swc/jest": "0.2.26", "@swc/jest": "0.2.26",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.2", "@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/body-parser": "^1.19.2",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.0", "@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/http-link-header": "^1.0.3",
"@types/jest": "29.5.2", "@types/jest": "29.5.2",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.1",
@@ -182,14 +186,15 @@
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31", "@types/ms": "^0.7.31",
"@types/node": "20.4.0", "@types/node": "20.3.1",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/oauth2orize": "^1.11.0",
"@types/pg": "8.10.2", "@types/pg": "8.10.2",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.5.1", "@types/qrcode": "1.5.0",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11", "@types/redis": "4.0.11",
@@ -197,6 +202,7 @@
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.5.0", "@types/semver": "7.5.0",
"@types/sharp": "0.32.0", "@types/sharp": "0.32.0",
"@types/simple-oauth2": "^5.0.4",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
@@ -206,14 +212,15 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.5", "@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.60.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.44.0", "eslint": "8.43.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "7.1.1", "execa": "6.1.0",
"jest": "29.6.1", "jest": "29.5.0",
"jest-mock": "29.6.1" "jest-mock": "29.5.0",
"simple-oauth2": "^5.0.0"
} }
} }

View File

@@ -0,0 +1,5 @@
declare module 'oauth2orize-pkce' {
export default {
extensions(): any;
};
}

View File

@@ -96,6 +96,12 @@ function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs'); const nodejsLogger = bootLogger.createSubLogger('nodejs');
nodejsLogger.info(`Version ${process.version} detected.`); 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 { function loadConfigBoot(): Config {

View File

@@ -295,7 +295,7 @@ export class AccountMoveService {
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる * dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
* *
* @param dst movedToUrlを指定するユーザー * @param dst movedToUrlを指定するユーザー
* @param check * @param check
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
* @returns Promise<LocalUser | RemoteUser | null> * @returns Promise<LocalUser | RemoteUser | null>
*/ */

View File

@@ -4,7 +4,6 @@ import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as nsfw from 'nsfwjs'; import * as nsfw from 'nsfwjs';
import si from 'systeminformation'; import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@@ -18,7 +17,6 @@ let isSupportedCpu: undefined | boolean = undefined;
@Injectable() @Injectable()
export class AiService { export class AiService {
private model: nsfw.NSFWJS; private model: nsfw.NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@@ -33,22 +31,16 @@ export class AiService {
const cpuFlags = await this.getCpuFlags(); const cpuFlags = await this.getCpuFlags();
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
} }
if (!isSupportedCpu) { if (!isSupportedCpu) {
console.error('This CPU cannot use TensorFlow.'); console.error('This CPU cannot use TensorFlow.');
return null; return null;
} }
const tf = await import('@tensorflow/tfjs-node'); const tf = await import('@tensorflow/tfjs-node');
if (this.model == null) { if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
}
});
}
const buffer = await fs.promises.readFile(path); const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as any; const image = await tf.node.decodeImage(buffer, 3) as any;
try { try {

View File

@@ -99,7 +99,7 @@ export class AntennaService implements OnApplicationShutdown {
'MAXLEN', '~', '200', 'MAXLEN', '~', '200',
'*', '*',
'note', note.id); 'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
} }
@@ -112,16 +112,16 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false; if (note.visibility === 'followers') return false;
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {
// TODO // TODO
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({ const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,
})).map(x => x.userId); })).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'users') { } else if (antenna.src === 'users') {
const accts = antenna.users.map(x => { const accts = antenna.users.map(x => {
@@ -130,32 +130,32 @@ export class AntennaService implements OnApplicationShutdown {
}); });
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = keywords.some(and => const matched = keywords.some(and =>
and.every(keyword => and.every(keyword =>
antenna.caseSensitive antenna.caseSensitive
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords const excludeKeywords = antenna.excludeKeywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
@@ -167,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown {
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (matched) return false; if (matched) return false;
} }
if (antenna.withFile) { if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false; if (note.fileIds && note.fileIds.length === 0) return false;
} }
// TODO: eval expression // TODO: eval expression
return true; return true;
} }
@@ -188,7 +188,7 @@ export class AntennaService implements OnApplicationShutdown {
}); });
this.antennasFetched = true; this.antennasFetched = true;
} }
return this.antennas; return this.antennas;
} }

View File

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

View File

@@ -20,7 +20,7 @@ export class CaptchaService {
secret, secret,
response, response,
}); });
const res = await this.httpRequestService.send(url, { const res = await this.httpRequestService.send(url, {
method: 'POST', method: 'POST',
body: params.toString(), body: params.toString(),
@@ -28,14 +28,14 @@ export class CaptchaService {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status}`); throw new Error(`${res.status}`);
} }
return await res.json() as CaptchaResponse; return await res.json() as CaptchaResponse;
} }
@bindThis @bindThis
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> { public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) { if (response == null) {
@@ -73,7 +73,7 @@ export class CaptchaService {
if (response == null) { if (response == null) {
throw new Error('turnstile-failed: no response provided'); throw new Error('turnstile-failed: no response provided');
} }
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
throw new Error(`turnstile-request-failed: ${err}`); throw new Error(`turnstile-request-failed: ${err}`);
}); });

View File

@@ -25,27 +25,27 @@ export class CreateSystemUserService {
@bindThis @bindThis
public async createSystemUser(username: string): Promise<User> { public async createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = uuid();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair(4096);
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error('the user is already exists'); if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(User, { account = await transactionalEntityManager.insert(User, {
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@@ -58,25 +58,25 @@ export class CreateSystemUserService {
isExplorable: false, isExplorable: false,
isBot: true, isBot: true,
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
await transactionalEntityManager.insert(UserKeypair, { await transactionalEntityManager.insert(UserKeypair, {
publicKey: keyPair.publicKey, publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey, privateKey: keyPair.privateKey,
userId: account.id, userId: account.id,
}); });
await transactionalEntityManager.insert(UserProfile, { await transactionalEntityManager.insert(UserProfile, {
userId: account.id, userId: account.id,
autoAcceptFollowed: false, autoAcceptFollowed: false,
password: hash, password: hash,
}); });
await transactionalEntityManager.insert(UsedUsername, { await transactionalEntityManager.insert(UsedUsername, {
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
}); });
}); });
return account; return account;
} }
} }

View File

@@ -140,7 +140,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated, emoji: updated,
}); });
} }
} }
@@ -194,7 +194,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', { this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
@@ -215,7 +215,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
} }
@bindThis @bindThis
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) { public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
await this.emojisRepository.update({ await this.emojisRepository.update({

View File

@@ -28,11 +28,11 @@ export class DeleteAccountService {
// 物理削除する前にDelete activityを送信する // 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.userSuspendService.doPostSuspend(user).catch(e => {});
this.queueService.createDeleteAccountJob(user, { this.queueService.createDeleteAccountJob(user, {
soft: false, soft: false,
}); });
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });

View File

@@ -29,12 +29,12 @@ export class EmailService {
@bindThis @bindThis
public async sendEmail(to: string, subject: string, html: string, text: string) { public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true); const meta = await this.metaService.fetch(true);
const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`; const emailSettingUrl = `${this.config.url}/settings/email`;
const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: meta.smtpHost, host: meta.smtpHost,
port: meta.smtpPort, port: meta.smtpPort,
@@ -46,7 +46,7 @@ export class EmailService {
pass: meta.smtpPass, pass: meta.smtpPass,
} : undefined, } : undefined,
} as any); } as any);
try { try {
// TODO: htmlサニタイズ // TODO: htmlサニタイズ
const info = await transporter.sendMail({ const info = await transporter.sendMail({
@@ -135,7 +135,7 @@ export class EmailService {
</body> </body>
</html>`, </html>`,
}); });
this.logger.info(`Message sent: ${info.messageId}`); this.logger.info(`Message sent: ${info.messageId}`);
} catch (err) { } catch (err) {
this.logger.error(err as Error); this.logger.error(err as Error);
@@ -149,12 +149,12 @@ export class EmailService {
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
}> { }> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const exist = await this.userProfilesRepository.countBy({ const exist = await this.userProfilesRepository.countBy({
emailVerified: true, emailVerified: true,
email: emailAddress, email: emailAddress,
}); });
const validated = meta.enableActiveEmailValidation ? await validateEmail({ const validated = meta.enableActiveEmailValidation ? await validateEmail({
email: emailAddress, email: emailAddress,
validateRegex: true, validateRegex: true,
@@ -163,9 +163,9 @@ export class EmailService {
validateDisposable: true, // 捨てアドかどうかチェック validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
}) : { valid: true, reason: null }; }) : { valid: true, reason: null };
const available = exist === 0 && validated.valid; const available = exist === 0 && validated.valid;
return { return {
available, available,
reason: available ? null : reason: available ? null :

View File

@@ -43,19 +43,19 @@ export class FederatedInstanceService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(host: string): Promise<Instance> { public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached; if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host }); const index = await this.instancesRepository.findOneBy({ host });
if (index == null) { if (index == null) {
const i = await this.instancesRepository.insert({ const i = await this.instancesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,
firstRetrievedAt: new Date(), firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.federatedInstanceCache.set(host, i); this.federatedInstanceCache.set(host, i);
return i; return i;
} else { } else {
@@ -74,7 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
.then((response) => { .then((response) => {
return response.raw[0]; return response.raw[0];
}); });
this.federatedInstanceCache.set(result.host, result); this.federatedInstanceCache.set(result.host, result);
} }

View File

@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import type { Instance } from '@/models/entities/Instance.js'; 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 type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
@@ -10,7 +12,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom'; import type { DOMWindow } from 'jsdom';
import * as Redis from 'ioredis';
type NodeInfo = { type NodeInfo = {
openRegistrations?: unknown; openRegistrations?: unknown;
@@ -36,49 +37,39 @@ export class FetchInstanceMetadataService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private appLockService: AppLockService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
@Inject(DI.redis)
private redisClient: Redis.Redis,
) { ) {
this.logger = this.loggerService.getLogger('metadata', 'cyan'); 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 @bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> { public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
const host = instance.host; const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host);
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return;
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`); if (!force) {
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();
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`);
try {
const [info, dom, manifest] = await Promise.all([ const [info, dom, manifest] = await Promise.all([
this.fetchNodeinfo(instance).catch(() => null), this.fetchNodeinfo(instance).catch(() => null),
this.fetchDom(instance).catch(() => null), this.fetchDom(instance).catch(() => null),
this.fetchManifest(instance).catch(() => null), this.fetchManifest(instance).catch(() => null),
]); ]);
const [favicon, icon, themeColor, name, description] = await Promise.all([ const [favicon, icon, themeColor, name, description] = await Promise.all([
this.fetchFaviconUrl(instance, dom).catch(() => null), this.fetchFaviconUrl(instance, dom).catch(() => null),
this.fetchIconUrl(instance, dom, manifest).catch(() => null), this.fetchIconUrl(instance, dom, manifest).catch(() => null),
@@ -86,13 +77,13 @@ export class FetchInstanceMetadataService {
this.getSiteName(info, dom, manifest).catch(() => null), this.getSiteName(info, dom, manifest).catch(() => null),
this.getDescription(info, dom, manifest).catch(() => null), this.getDescription(info, dom, manifest).catch(() => null),
]); ]);
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
const updates = { const updates = {
infoUpdatedAt: new Date(), infoUpdatedAt: new Date(),
} as Record<string, any>; } as Record<string, any>;
if (info) { if (info) {
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?'; updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
updates.softwareVersion = info.software?.version; updates.softwareVersion = info.software?.version;
@@ -100,27 +91,27 @@ export class FetchInstanceMetadataService {
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
} }
if (name) updates.name = name; if (name) updates.name = name;
if (description) updates.description = description; if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon; if (icon || favicon) updates.iconUrl = icon ?? favicon;
if (favicon) updates.faviconUrl = favicon; if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor; if (themeColor) updates.themeColor = themeColor;
await this.federatedInstanceService.update(instance.id, updates); await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`); this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
} catch (e) { } catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
await this.unlock(host); unlock();
} }
} }
@bindThis @bindThis
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> { private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try { try {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
.catch(err => { .catch(err => {
@@ -130,33 +121,33 @@ export class FetchInstanceMetadataService {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
} }
}) as Record<string, unknown>; }) as Record<string, unknown>;
if (wellknown.links == null || !Array.isArray(wellknown.links)) { if (wellknown.links == null || !Array.isArray(wellknown.links)) {
throw new Error('No wellknown links'); throw new Error('No wellknown links');
} }
const links = wellknown.links as any[]; const links = wellknown.links as any[];
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
if (link == null) { if (link == null) {
throw new Error('No nodeinfo link provided'); throw new Error('No nodeinfo link provided');
} }
const info = await this.httpRequestService.getJson(link.href) const info = await this.httpRequestService.getJson(link.href)
.catch(err => { .catch(err => {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
}); });
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
return info as NodeInfo; return info as NodeInfo;
} catch (err) { } catch (err) {
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
throw err; throw err;
} }
} }
@@ -164,51 +155,51 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> { private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
this.logger.info(`Fetching HTML of ${instance.host} ...`); this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html); const { window } = new JSDOM(html);
const doc = window.document; const doc = window.document;
return doc; return doc;
} }
@bindThis @bindThis
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> { private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const manifestUrl = url + '/manifest.json'; const manifestUrl = url + '/manifest.json';
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>; const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
return manifest; return manifest;
} }
@bindThis @bindThis
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> { private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
if (doc) { if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
const faviconUrl = url + '/favicon.ico'; const faviconUrl = url + '/favicon.ico';
const favicon = await this.httpRequestService.send(faviconUrl, { const favicon = await this.httpRequestService.send(faviconUrl, {
method: 'HEAD', method: 'HEAD',
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (favicon.ok) { if (favicon.ok) {
return faviconUrl; return faviconUrl;
} }
return null; return null;
} }
@@ -218,38 +209,38 @@ export class FetchInstanceMetadataService {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href; return (new URL(manifest.icons[0].src, url)).href;
} }
if (doc) { if (doc) {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const links = Array.from(doc.getElementsByTagName('link')).reverse(); const links = Array.from(doc.getElementsByTagName('link')).reverse();
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
const href = const href =
[ [
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.relList.contains('apple-touch-icon'))?.href,
links.find(link => link.relList.contains('icon'))?.href, links.find(link => link.relList.contains('icon'))?.href,
] ]
.find(href => href); .find(href => href);
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
return null; return null;
} }
@bindThis @bindThis
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
if (themeColor) { if (themeColor) {
const color = new tinycolor(themeColor); const color = new tinycolor(themeColor);
if (color.isValid()) return color.toHexString(); if (color.isValid()) return color.toHexString();
} }
return null; return null;
} }
@@ -262,19 +253,19 @@ export class FetchInstanceMetadataService {
return info.metadata.name; return info.metadata.name;
} }
} }
if (doc) { if (doc) {
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
@@ -287,23 +278,23 @@ export class FetchInstanceMetadataService {
return info.metadata.description; return info.metadata.description;
} }
} }
if (doc) { if (doc) {
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
if (meta) { if (meta) {
return meta; return meta;
} }
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
} }

View File

@@ -161,20 +161,20 @@ export class FileInfoService {
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
return [sensitive, porn]; return [sensitive, porn];
} }
if ([ if ([
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@@ -253,10 +253,10 @@ export class FileInfoService {
disposeOutDir(); disposeOutDir();
} }
} }
return [sensitive, porn]; return [sensitive, porn];
} }
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> { private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
const watcher = new FSWatcher({ const watcher = new FSWatcher({
cwd, cwd,
@@ -295,7 +295,7 @@ export class FileInfoService {
} }
} }
} }
@bindThis @bindThis
private exists(path: string): Promise<boolean> { private exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false); return fs.promises.access(path).then(() => true, () => false);
@@ -304,11 +304,11 @@ export class FileInfoService {
@bindThis @bindThis
public fixMime(mime: string | fileType.MimeType): string { public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686 // see https://github.com/misskey-dev/misskey/pull/10686
if (mime === 'audio/x-flac') { if (mime === "audio/x-flac") {
return 'audio/flac'; return "audio/flac";
} }
if (mime === 'audio/vnd.wave') { if (mime === "audio/vnd.wave") {
return 'audio/wav'; return "audio/wav";
} }
return mime; return mime;
@@ -355,12 +355,11 @@ export class FileInfoService {
* Check the file is SVG or not * Check the file is SVG or not
*/ */
@bindThis @bindThis
public async checkSvg(path: string): Promise<boolean> { public async checkSvg(path: string) {
try { try {
const size = await this.getFileSize(path); const size = await this.getFileSize(path);
if (size > 1 * 1024 * 1024) return false; if (size > 1 * 1024 * 1024) return false;
const buffer = await fs.promises.readFile(path); return isSvg(fs.readFileSync(path));
return isSvg(buffer.toString());
} catch { } catch {
return false; return false;
} }

View File

@@ -42,21 +42,21 @@ export class HttpRequestService {
errorTtl: 30, // 30secs errorTtl: 30, // 30secs
lookup: false, // nativeのdns.lookupにfallbackしない lookup: false, // nativeのdns.lookupにfallbackしない
}); });
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as http.AgentOptions); } as http.AgentOptions);
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as https.AgentOptions); } as https.AgentOptions);
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
this.httpAgent = config.proxy this.httpAgent = config.proxy
? new HttpProxyAgent({ ? new HttpProxyAgent({
keepAlive: true, keepAlive: true,

View File

@@ -23,7 +23,7 @@ export class IdService {
@bindThis @bindThis
public genId(date?: Date): string { public genId(date?: Date): string {
if (!date || (date > new Date())) date = new Date(); if (!date || (date > new Date())) date = new Date();
switch (this.method) { switch (this.method) {
case 'aid': return genAid(date); case 'aid': return genAid(date);
case 'meid': return genMeid(date); case 'meid': return genMeid(date);

View File

@@ -26,12 +26,12 @@ export class InstanceActorService {
public async getInstanceActor(): Promise<LocalUser> { public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(); const cached = this.cache.get();
if (cached) return cached; if (cached) return cached;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}) as LocalUser | undefined; }) as LocalUser | undefined;
if (user) { if (user) {
this.cache.set(user); this.cache.set(user);
return user; return user;

View File

@@ -56,7 +56,7 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(noCache = false): Promise<Meta> { public async fetch(noCache = false): Promise<Meta> {
if (!noCache && this.cache) return this.cache; if (!noCache && this.cache) return this.cache;
return await this.db.transaction(async transactionalEntityManager => { return await this.db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(Meta, { const metas = await transactionalEntityManager.find(Meta, {
@@ -64,9 +64,9 @@ export class MetaService implements OnApplicationShutdown {
id: 'DESC', id: 'DESC',
}, },
}); });
const meta = metas[0]; const meta = metas[0];
if (meta) { if (meta) {
this.cache = meta; this.cache = meta;
return meta; return meta;
@@ -81,7 +81,7 @@ export class MetaService implements OnApplicationShutdown {
['id'], ['id'],
) )
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
this.cache = saved; this.cache = saved;
return saved; return saved;
} }

View File

@@ -27,29 +27,29 @@ export class MfmService {
public fromHtml(html: string, hashtagNames?: string[]): string { public fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines // some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n'); html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
const dom = parse5.parseFragment(html); const dom = parse5.parseFragment(html);
let text = ''; let text = '';
for (const n of dom.childNodes) { for (const n of dom.childNodes) {
analyze(n); analyze(n);
} }
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
if (node.childNodes) { if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join(''); return node.childNodes.map(n => getText(n)).join('');
} }
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
@@ -57,35 +57,35 @@ export class MfmService {
} }
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: TreeAdapter.Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
text += '\n'; text += '\n';
break; break;
} }
case 'a': case 'a':
{ {
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ // ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${(new URL(href.value)).hostname}`;
@@ -116,12 +116,12 @@ export class MfmService {
return `[${txt}](${href.value})`; return `[${txt}](${href.value})`;
} }
}; };
text += generateLink(); text += generateLink();
} }
break; break;
} }
case 'h1': case 'h1':
{ {
text += '【'; text += '【';
@@ -129,7 +129,7 @@ export class MfmService {
text += '】\n'; text += '】\n';
break; break;
} }
case 'b': case 'b':
case 'strong': case 'strong':
{ {
@@ -138,7 +138,7 @@ export class MfmService {
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small':
{ {
text += '<small>'; text += '<small>';
@@ -146,7 +146,7 @@ export class MfmService {
text += '</small>'; text += '</small>';
break; break;
} }
case 's': case 's':
case 'del': case 'del':
{ {
@@ -155,7 +155,7 @@ export class MfmService {
text += '~~'; text += '~~';
break; break;
} }
case 'i': case 'i':
case 'em': case 'em':
{ {
@@ -164,7 +164,7 @@ export class MfmService {
text += '</i>'; text += '</i>';
break; break;
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case 'pre': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@@ -176,7 +176,7 @@ export class MfmService {
} }
break; break;
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case 'code': {
text += '`'; text += '`';
@@ -184,7 +184,7 @@ export class MfmService {
text += '`'; text += '`';
break; break;
} }
case 'blockquote': { case 'blockquote': {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
@@ -193,7 +193,7 @@ export class MfmService {
} }
break; break;
} }
case 'p': case 'p':
case 'h2': case 'h2':
case 'h3': case 'h3':
@@ -205,7 +205,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case 'div':
case 'header': case 'header':
@@ -219,7 +219,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: // includes inline elements
{ {
appendChildren(node.childNodes); appendChildren(node.childNodes);
@@ -234,48 +234,48 @@ export class MfmService {
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new Window(); const { window } = new Window();
const doc = window.document; const doc = window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
} }
} }
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
bold: (node) => { bold: (node) => {
const el = doc.createElement('b'); const el = doc.createElement('b');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
small: (node) => { small: (node) => {
const el = doc.createElement('small'); const el = doc.createElement('small');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
strike: (node) => { strike: (node) => {
const el = doc.createElement('del'); const el = doc.createElement('del');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
italic: (node) => { italic: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
fn: (node) => { fn: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
blockCode: (node) => { blockCode: (node) => {
const pre = doc.createElement('pre'); const pre = doc.createElement('pre');
const inner = doc.createElement('code'); const inner = doc.createElement('code');
@@ -283,21 +283,21 @@ export class MfmService {
pre.appendChild(inner); pre.appendChild(inner);
return pre; return pre;
}, },
center: (node) => { center: (node) => {
const el = doc.createElement('div'); const el = doc.createElement('div');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
emojiCode: (node) => { emojiCode: (node) => {
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
}, },
unicodeEmoji: (node) => { unicodeEmoji: (node) => {
return doc.createTextNode(node.props.emoji); return doc.createTextNode(node.props.emoji);
}, },
hashtag: (node) => { hashtag: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
@@ -305,32 +305,32 @@ export class MfmService {
a.setAttribute('rel', 'tag'); a.setAttribute('rel', 'tag');
return a; return a;
}, },
inlineCode: (node) => { inlineCode: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.code; el.textContent = node.props.code;
return el; return el;
}, },
mathInline: (node) => { mathInline: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
mathBlock: (node) => { mathBlock: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
link: (node) => { link: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
appendChildren(node.children, a); appendChildren(node.children, a);
return a; return a;
}, },
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
@@ -340,47 +340,47 @@ export class MfmService {
a.textContent = acct; a.textContent = acct;
return a; return a;
}, },
quote: (node) => { quote: (node) => {
const el = doc.createElement('blockquote'); const el = doc.createElement('blockquote');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
text: (node) => { text: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x); el.appendChild(x === 'br' ? doc.createElement('br') : x);
} }
return el; return el;
}, },
url: (node) => { url: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
a.textContent = node.props.url; a.textContent = node.props.url;
return a; return a;
}, },
search: (node) => { search: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`); a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
a.textContent = node.props.content; a.textContent = node.props.content;
return a; return a;
}, },
plain: (node) => { plain: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
}; };
appendChildren(nodes, doc.body); appendChildren(nodes, doc.body);
return `<p>${doc.body.innerHTML}</p>`; return `<p>${doc.body.innerHTML}</p>`;
} }
} }

View File

@@ -672,7 +672,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Register to search database // Register to search database
this.index(note); this.index(note);
} }
@bindThis @bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean { private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) { if (sensitiveWord.length > 0) {
@@ -758,7 +758,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private index(note: Note) { private index(note: Note) {
if (note.text == null && note.cw == null) return; if (note.text == null && note.cw == null) return;
this.searchService.indexNote(note); this.searchService.indexNote(note);
} }

View File

@@ -45,7 +45,7 @@ export class NoteDeleteService {
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) {} ) {}
/** /**
* 投稿を削除します。 * 投稿を削除します。
* @param user 投稿者 * @param user 投稿者
@@ -121,8 +121,10 @@ export class NoteDeleteService {
} }
@bindThis @bindThis
private async findCascadingNotes(note: Note): Promise<Note[]> { private async findCascadingNotes(note: Note) {
const recursive = async (noteId: string): Promise<Note[]> => { const cascadingNotes: Note[] = [];
const recursive = async (noteId: string) => {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId }) .where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => { .orWhere(new Brackets(q => {
@@ -131,14 +133,12 @@ export class NoteDeleteService {
})) }))
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany(); const replies = await query.getMany();
for (const reply of replies) {
return [ cascadingNotes.push(reply);
replies, await recursive(reply.id);
...await Promise.all(replies.map(reply => recursive(reply.id))), }
].flat();
}; };
await recursive(note.id);
const cascadingNotes: Note[] = await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
} }

View File

@@ -99,7 +99,7 @@ export class NoteReadService implements OnApplicationShutdown {
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isMentioned: true, isMentioned: true,
@@ -109,7 +109,7 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
}); });
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isSpecified: true, isSpecified: true,

View File

@@ -46,7 +46,7 @@ export class NotificationService implements OnApplicationShutdown {
force = false, force = false,
) { ) {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange( const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`, `notificationTimeline:${userId}`,
'+', '+',

View File

@@ -39,12 +39,12 @@ export class PollService {
@bindThis @bindThis
public async vote(user: User, note: Note, choice: number) { public async vote(user: User, note: Note, choice: number) {
const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('poll not found'); if (poll == null) throw new Error('poll not found');
// Check whether is valid choice // Check whether is valid choice
if (poll.choices[choice] == null) throw new Error('invalid choice param'); if (poll.choices[choice] == null) throw new Error('invalid choice param');
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -52,13 +52,13 @@ export class PollService {
throw new Error('blocked'); throw new Error('blocked');
} }
} }
// if already voted // if already voted
const exist = await this.pollVotesRepository.findBy({ const exist = await this.pollVotesRepository.findBy({
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
if (poll.multiple) { if (poll.multiple) {
if (exist.some(x => x.choice === choice)) { if (exist.some(x => x.choice === choice)) {
throw new Error('already voted'); throw new Error('already voted');
@@ -66,7 +66,7 @@ export class PollService {
} else if (exist.length !== 0) { } else if (exist.length !== 0) {
throw new Error('already voted'); throw new Error('already voted');
} }
// Create vote // Create vote
await this.pollVotesRepository.insert({ await this.pollVotesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@@ -75,11 +75,11 @@ export class PollService {
userId: user.id, userId: user.id,
choice: choice, choice: choice,
}); });
// Increment votes count // Increment votes count
const index = choice + 1; // In SQL, array index is 1 based const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', { this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
choice: choice, choice: choice,
userId: user.id, userId: user.id,
@@ -90,10 +90,10 @@ export class PollService {
public async deliverQuestionUpdate(noteId: Note['id']) { public async deliverQuestionUpdate(noteId: Note['id']) {
const note = await this.notesRepository.findOneBy({ id: noteId }); const note = await this.notesRepository.findOneBy({ id: noteId });
if (note == null) throw new Error('note not found'); if (note == null) throw new Error('note not found');
const user = await this.usersRepository.findOneBy({ id: note.userId }); const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) throw new Error('note not found'); if (user == null) throw new Error('note not found');
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
this.apDeliverManagerService.deliverToFollowers(user, content); this.apDeliverManagerService.deliverToFollowers(user, content);

View File

@@ -31,7 +31,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
...body.note, ...body.note,
// textをgetNoteSummaryしたものに置き換える // textをgetNoteSummaryしたものに置き換える
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note), text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
cw: undefined, cw: undefined,
reply: undefined, reply: undefined,
renote: undefined, renote: undefined,
@@ -69,16 +69,16 @@ export class PushNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) { public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(this.config.url, push.setVapidDetails(this.config.url,
meta.swPublicKey, meta.swPublicKey,
meta.swPrivateKey); meta.swPrivateKey);
const subscriptions = await this.subscriptionsCache.fetch(userId); const subscriptions = await this.subscriptionsCache.fetch(userId);
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
if ([ if ([
'readAllNotifications', 'readAllNotifications',
@@ -103,7 +103,7 @@ export class PushNotificationService implements OnApplicationShutdown {
//swLogger.info(err.statusCode); //swLogger.info(err.statusCode);
//swLogger.info(err.headers); //swLogger.info(err.headers);
//swLogger.info(err.body); //swLogger.info(err.body);
if (err.statusCode === 410) { if (err.statusCode === 410) {
this.swSubscriptionsRepository.delete({ this.swSubscriptionsRepository.delete({
userId: userId, userId: userId,

View File

@@ -60,8 +60,8 @@ export class QueryService {
q.orderBy(`${q.alias}.id`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} }
return q; return q;
} }
// ここでいうBlockedは被Blockedの意 // ここでいうBlockedは被Blockedの意
@bindThis @bindThis
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void { public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
@@ -109,18 +109,18 @@ export class QueryService {
q.andWhere('note.channelId IS NULL'); q.andWhere('note.channelId IS NULL');
} else { } else {
q.leftJoinAndSelect('note.channel', 'channel'); q.leftJoinAndSelect('note.channel', 'channel');
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
.select('channelFollowing.followeeId') .select('channelFollowing.followeeId')
.where('channelFollowing.followerId = :followerId', { followerId: me.id }); .where('channelFollowing.followerId = :followerId', { followerId: me.id });
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// チャンネルのノートではない // チャンネルのノートではない
.where('note.channelId IS NULL') .where('note.channelId IS NULL')
// または自分がフォローしているチャンネルのノート // または自分がフォローしているチャンネルのノート
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
})); }));
q.setParameters(channelFollowingQuery.getParameters()); q.setParameters(channelFollowingQuery.getParameters());
} }
} }
@@ -130,9 +130,9 @@ export class QueryService {
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
.select('muted.noteId') .select('muted.noteId')
.where('muted.userId = :userId', { userId: me.id }); .where('muted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@@ -141,13 +141,13 @@ export class QueryService {
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
.select('threadMuted.threadId') .select('threadMuted.threadId')
.where('threadMuted.userId = :userId', { userId: me.id }); .where('threadMuted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
.where('note.threadId IS NULL') .where('note.threadId IS NULL')
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
})); }));
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@@ -156,15 +156,15 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
if (exclude) { if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
} }
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances') .select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id }); .where('user_profile.userId = :muterId', { muterId: me.id });
// 投稿の作者をミュートしていない かつ // 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない // 投稿の引用元の作者をミュートしていない
@@ -191,7 +191,7 @@ export class QueryService {
.where('note.renoteUserHost IS NULL') .where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters()); q.setParameters(mutingInstanceQuery.getParameters());
} }
@@ -201,9 +201,9 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
@@ -245,7 +245,7 @@ export class QueryService {
const followingQuery = this.followingsRepository.createQueryBuilder('following') const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId') .select('following.followeeId')
.where('following.followerId = :meId'); .where('following.followerId = :meId');
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// 公開投稿である // 公開投稿である
.where(new Brackets(qb => { qb .where(new Brackets(qb => { qb
@@ -268,7 +268,7 @@ export class QueryService {
})); }));
})); }));
})); }));
q.setParameters({ meId: me.id }); q.setParameters({ meId: me.id });
} }
} }
@@ -278,10 +278,10 @@ export class QueryService {
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
.select('renote_muting.muteeId') .select('renote_muting.muteeId')
.where('renote_muting.muterId = :muterId', { muterId: me.id }); .where('renote_muting.muterId = :muterId', { muterId: me.id });
q.andWhere(new Brackets(qb => { q.andWhere(new Brackets(qb => {
qb qb
.where(new Brackets(qb => { .where(new Brackets(qb => {
qb.where('note.renoteId IS NOT NULL'); qb.where('note.renoteId IS NOT NULL');
qb.andWhere('note.text IS NULL'); qb.andWhere('note.text IS NULL');
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
@@ -289,7 +289,7 @@ export class QueryService {
.orWhere('note.renoteId IS NULL') .orWhere('note.renoteId IS NULL')
.orWhere('note.text IS NOT NULL'); .orWhere('note.text IS NOT NULL');
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
} }

View File

@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.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 { 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 httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@@ -69,7 +69,7 @@ export class QueueService {
if (content == null) return null; if (content == null) return null;
if (to == null) return null; if (to == null) return null;
const data: DeliverJobData = { const data = {
user: { user: {
id: user.id, 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 @bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
const data = { const data = {

View File

@@ -39,9 +39,9 @@ export class RelayService {
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}); });
if (user) return user as LocalUser; if (user) return user as LocalUser;
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
return created as LocalUser; return created as LocalUser;
} }
@@ -53,12 +53,12 @@ export class RelayService {
inbox, inbox,
status: 'requesting', status: 'requesting',
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay; return relay;
} }
@@ -67,17 +67,17 @@ export class RelayService {
const relay = await this.relaysRepository.findOneBy({ const relay = await this.relaysRepository.findOneBy({
inbox, inbox,
}); });
if (relay == null) { if (relay == null) {
throw new Error('relay not found'); throw new Error('relay not found');
} }
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
await this.relaysRepository.delete(relay.id); await this.relaysRepository.delete(relay.id);
} }
@@ -86,13 +86,13 @@ export class RelayService {
const relays = await this.relaysRepository.find(); const relays = await this.relaysRepository.find();
return relays; return relays;
} }
@bindThis @bindThis
public async relayAccepted(id: string): Promise<string> { public async relayAccepted(id: string): Promise<string> {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'accepted', status: 'accepted',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@@ -101,24 +101,24 @@ export class RelayService {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'rejected', status: 'rejected',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@bindThis @bindThis
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return; if (activity == null) return;
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted', status: 'accepted',
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;
const copy = deepClone(activity); const copy = deepClone(activity);
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await this.apRendererService.attachLdSignature(copy, user); const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) { for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox, false); this.queueService.deliver(user, signed, relay.inbox, false);
} }

View File

@@ -35,7 +35,7 @@ export class RemoteUserResolveService {
@bindThis @bindThis
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> { public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
const usernameLower = username.toLowerCase(); const usernameLower = username.toLowerCase();
if (host == null) { if (host == null) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@@ -46,9 +46,9 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (this.config.host === host) { if (this.config.host === host) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@@ -59,39 +59,39 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
const acctLower = `${usernameLower}@${host}`; const acctLower = `${usernameLower}@${host}`;
if (user == null) { if (user == null) {
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す // ユーザー情報が古い場合は、WebFilgerからやりなおして返す
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
}); });
this.logger.info(`try resync: ${acctLower}`); this.logger.info(`try resync: ${acctLower}`);
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
if (user.uri !== self.href) { if (user.uri !== self.href) {
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`uri missmatch: ${acctLower}`);
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
// validate uri // validate uri
const uri = new URL(self.href); const uri = new URL(self.href);
if (uri.hostname !== host) { if (uri.hostname !== host) {
throw new Error('Invalid uri'); throw new Error('Invalid uri');
} }
await this.usersRepository.update({ await this.usersRepository.update({
usernameLower, usernameLower,
host: host, host: host,
@@ -101,9 +101,9 @@ export class RemoteUserResolveService {
} else { } else {
this.logger.info(`uri is fine: ${acctLower}`); this.logger.info(`uri is fine: ${acctLower}`);
} }
await this.apPersonService.updatePerson(self.href); await this.apPersonService.updatePerson(self.href);
this.logger.info(`return resynced remote user: ${acctLower}`); this.logger.info(`return resynced remote user: ${acctLower}`);
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
if (u == null) { if (u == null) {
@@ -113,7 +113,7 @@ export class RemoteUserResolveService {
} }
}); });
} }
this.logger.info(`return existing remote user: ${acctLower}`); this.logger.info(`return existing remote user: ${acctLower}`);
return user; return user;
} }

View File

@@ -392,7 +392,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> { public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
const now = new Date(); const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
if (existing == null) { if (existing == null) {
throw new RoleService.NotAssignedError(); throw new RoleService.NotAssignedError();

View File

@@ -174,7 +174,7 @@ export class SearchService {
if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(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

@@ -50,31 +50,31 @@ export class SignupService {
}) { }) {
const { username, password, passwordHash, host } = opts; const { username, password, passwordHash, host } = opts;
let hash = passwordHash; let hash = passwordHash;
// Validate username // Validate username
if (!this.userEntityService.validateLocalUsername(username)) { if (!this.userEntityService.validateLocalUsername(username)) {
throw new Error('INVALID_USERNAME'); throw new Error('INVALID_USERNAME');
} }
if (password != null && passwordHash == null) { if (password != null && passwordHash == null) {
// Validate password // Validate password
if (!this.userEntityService.validatePassword(password)) { if (!this.userEntityService.validatePassword(password)) {
throw new Error('INVALID_PASSWORD'); throw new Error('INVALID_PASSWORD');
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
hash = await bcrypt.hash(password, salt); hash = await bcrypt.hash(password, salt);
} }
// Generate secret // Generate secret
const secret = generateUserToken(); const secret = generateUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
throw new Error('DUPLICATED_USERNAME'); throw new Error('DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
@@ -106,18 +106,18 @@ export class SignupService {
}, (err, publicKey, privateKey) => }, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error(' the username is already used'); if (exist) throw new Error(' the username is already used');
account = await transactionalEntityManager.save(new User({ account = await transactionalEntityManager.save(new User({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@@ -127,27 +127,27 @@ export class SignupService {
token: secret, token: secret,
isRoot: isTheFirstUser, isRoot: isTheFirstUser,
})); }));
await transactionalEntityManager.save(new UserKeypair({ await transactionalEntityManager.save(new UserKeypair({
publicKey: keyPair[0], publicKey: keyPair[0],
privateKey: keyPair[1], privateKey: keyPair[1],
userId: account.id, userId: account.id,
})); }));
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: account.id, userId: account.id,
autoAcceptFollowed: true, autoAcceptFollowed: true,
password: hash, password: hash,
})); }));
await transactionalEntityManager.save(new UsedUsername({ await transactionalEntityManager.save(new UsedUsername({
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
})); }));
}); });
this.usersChart.update(account, true); this.usersChart.update(account, true);
return { account, secret }; return { account, secret };
} }
} }

View File

@@ -69,7 +69,7 @@ function verifyCertificateChain(certificates: string[]) {
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
if (certStruct == null) throw new Error('certStruct is null'); if (certStruct == null) throw new Error('certStruct is null');
const algorithm = certificate.getSignatureAlgorithmField(); const algorithm = certificate.getSignatureAlgorithmField();
const signatureHex = certificate.getSignatureValueHex(); const signatureHex = certificate.getSignatureValueHex();
@@ -143,19 +143,19 @@ export class TwoFactorAuthenticationService {
if (clientData.type !== 'webauthn.get') { if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get'); throw new Error('type is not webauthn.get');
} }
if (this.hash(clientData.challenge).toString('hex') !== challenge) { if (this.hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch'); throw new Error('challenge mismatch');
} }
if (clientData.origin !== this.config.scheme + '://' + this.config.host) { if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
throw new Error('origin mismatch'); throw new Error('origin mismatch');
} }
const verificationData = Buffer.concat( const verificationData = Buffer.concat(
[authenticatorData, this.hash(clientDataJSON)], [authenticatorData, this.hash(clientDataJSON)],
32 + authenticatorData.length, 32 + authenticatorData.length,
); );
return crypto return crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
@@ -168,7 +168,7 @@ export class TwoFactorAuthenticationService {
none: { none: {
verify({ publicKey }: { publicKey: Map<number, Buffer> }) { verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -176,12 +176,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
publicKey: publicKeyU2F, publicKey: publicKeyU2F,
valid: true, valid: true,
@@ -207,16 +207,16 @@ export class TwoFactorAuthenticationService {
if (attStmt.alg !== -7) { if (attStmt.alg !== -7) {
throw new Error('alg mismatch'); throw new Error('alg mismatch');
} }
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
const attCert: Buffer = attStmt.x5c[0]; const attCert: Buffer = attStmt.x5c[0];
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -224,23 +224,23 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
if (!attCert.equals(publicKeyData)) { if (!attCert.equals(publicKeyData)) {
throw new Error('public key mismatch'); throw new Error('public key mismatch');
} }
const isValid = crypto const isValid = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
return { return {
valid: isValid, valid: isValid,
publicKey: publicKeyData, publicKey: publicKeyData,
@@ -267,43 +267,43 @@ export class TwoFactorAuthenticationService {
const verificationData = this.hash( const verificationData = this.hash(
Buffer.concat([authenticatorData, clientDataHash]), Buffer.concat([authenticatorData, clientDataHash]),
); );
const jwsParts = attStmt.response.toString('utf-8').split('.'); const jwsParts = attStmt.response.toString('utf-8').split('.');
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
const response = JSON.parse( const response = JSON.parse(
base64URLDecode(jwsParts[1]).toString('utf-8'), base64URLDecode(jwsParts[1]).toString('utf-8'),
); );
const signature = jwsParts[2]; const signature = jwsParts[2];
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
throw new Error('invalid nonce'); throw new Error('invalid nonce');
} }
const certificateChain = header.x5c const certificateChain = header.x5c
.map((key: any) => PEMString(key)) .map((key: any) => PEMString(key))
.concat([GSR2]); .concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name'); throw new Error('invalid common name');
} }
if (!verifyCertificateChain(certificateChain)) { if (!verifyCertificateChain(certificateChain)) {
throw new Error('Invalid certificate chain!'); throw new Error('Invalid certificate chain!');
} }
const signatureBase = Buffer.from( const signatureBase = Buffer.from(
jwsParts[0] + '.' + jwsParts[1], jwsParts[0] + '.' + jwsParts[1],
'utf-8', 'utf-8',
); );
const valid = crypto const valid = crypto
.createVerify('sha256') .createVerify('sha256')
.update(signatureBase) .update(signatureBase)
.verify(certificateChain[0], base64URLDecode(signature)); .verify(certificateChain[0], base64URLDecode(signature));
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -311,7 +311,7 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
@@ -342,17 +342,17 @@ export class TwoFactorAuthenticationService {
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
if (attStmt.x5c) { if (attStmt.x5c) {
const attCert = attStmt.x5c[0]; const attCert = attStmt.x5c[0];
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -360,12 +360,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyData, publicKey: publicKeyData,
@@ -375,12 +375,12 @@ export class TwoFactorAuthenticationService {
throw new Error('ECDAA-Verify is not supported'); throw new Error('ECDAA-Verify is not supported');
} else { } else {
if (attStmt.alg !== -7) throw new Error('alg mismatch'); if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported'); throw new Error('self attestation is not supported');
} }
}, },
}, },
'fido-u2f': { 'fido-u2f': {
verify({ verify({
attStmt, attStmt,
@@ -401,13 +401,13 @@ export class TwoFactorAuthenticationService {
if (x5c.length !== 1) { if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation'); throw new Error('x5c length does not match expectation');
} }
const attCert = x5c[0]; const attCert = x5c[0];
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
const negTwo: Buffer = publicKey.get(-2); const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@@ -415,12 +415,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
NULL_BYTE, NULL_BYTE,
rpIdHash, rpIdHash,
@@ -428,12 +428,12 @@ export class TwoFactorAuthenticationService {
credentialId, credentialId,
publicKeyU2F, publicKeyU2F,
]); ]);
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyU2F, publicKey: publicKeyU2F,

View File

@@ -32,13 +32,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> { public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信 // 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@@ -46,13 +46,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true); this.queueService.deliver(user, content, inbox, true);
} }
@@ -62,13 +62,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostUnsuspend(user: User): Promise<void> { public async doPostUnsuspend(user: User): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信 // 知り得る全SharedInboxにUndo Delete配信
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@@ -76,13 +76,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox, true); this.queueService.deliver(user as any, content, inbox, true);
} }

View File

@@ -21,7 +21,7 @@ export class VideoProcessingService {
@bindThis @bindThis
public async generateVideoThumbnail(source: string): Promise<IImage> { public async generateVideoThumbnail(source: string): Promise<IImage> {
const [dir, cleanup] = await createTempDir(); const [dir, cleanup] = await createTempDir();
try { try {
await new Promise((res, rej) => { await new Promise((res, rej) => {
FFmpeg({ FFmpeg({

View File

@@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
}); });
this.webhooksFetched = true; this.webhooksFetched = true;
} }
return this.webhooks; return this.webhooks;
} }

View File

@@ -27,14 +27,14 @@ export class ApAudienceService {
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> { public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
const toGroups = this.groupingAudience(getApIds(to), actor); const toGroups = this.groupingAudience(getApIds(to), actor);
const ccGroups = this.groupingAudience(getApIds(cc), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor);
const others = unique(concat([toGroups.other, ccGroups.other])); const others = unique(concat([toGroups.other, ccGroups.other]));
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter((x): x is User => x != null); )).filter((x): x is User => x != null);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {
visibility: 'public', visibility: 'public',
@@ -42,7 +42,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (ccGroups.public.length > 0) { if (ccGroups.public.length > 0) {
return { return {
visibility: 'home', visibility: 'home',
@@ -50,7 +50,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (toGroups.followers.length > 0) { if (toGroups.followers.length > 0) {
return { return {
visibility: 'followers', visibility: 'followers',
@@ -58,14 +58,14 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
return { return {
visibility: 'specified', visibility: 'specified',
mentionedUsers, mentionedUsers,
visibleUsers: mentionedUsers, visibleUsers: mentionedUsers,
}; };
} }
@bindThis @bindThis
private groupingAudience(ids: string[], actor: RemoteUser) { private groupingAudience(ids: string[], actor: RemoteUser) {
const groups = { const groups = {
@@ -73,7 +73,7 @@ export class ApAudienceService {
followers: [] as string[], followers: [] as string[],
other: [] as string[], other: [] as string[],
}; };
for (const id of ids) { for (const id of ids) {
if (this.isPublic(id)) { if (this.isPublic(id)) {
groups.public.push(id); groups.public.push(id);
@@ -83,12 +83,12 @@ export class ApAudienceService {
groups.other.push(id); groups.other.push(id);
} }
} }
groups.other = unique(groups.other); groups.other = unique(groups.other);
return groups; return groups;
} }
@bindThis @bindThis
private isPublic(id: string) { private isPublic(id: string) {
return [ return [
@@ -97,7 +97,7 @@ export class ApAudienceService {
'Public', 'Public',
].includes(id); ].includes(id);
} }
@bindThis @bindThis
private isFollowers(id: string, actor: RemoteUser) { private isFollowers(id: string, actor: RemoteUser) {
return ( return (

View File

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

View File

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

@@ -21,7 +21,7 @@ export class ApMfmService {
@bindThis @bindThis
public htmlToMfm(html: string, tag?: IObject | IObject[]) { public htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
return this.mfmService.fromHtml(html, hashtagNames); return this.mfmService.fromHtml(html, hashtagNames);
} }
@@ -29,5 +29,5 @@ export class ApMfmService {
public getNoteHtml(note: Note) { public getNoteHtml(note: Note) {
if (!note.text) return ''; if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
} }
} }

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.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 { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApDbResolverService } from '../ApDbResolverService.js';
@@ -54,7 +55,7 @@ export class ApNoteService {
// 循環参照のため / for circular dependency // 循環参照のため / for circular dependency
@Inject(forwardRef(() => ApPersonService)) @Inject(forwardRef(() => ApPersonService))
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private utilityService: UtilityService, private utilityService: UtilityService,
private apAudienceService: ApAudienceService, private apAudienceService: ApAudienceService,
private apMentionService: ApMentionService, private apMentionService: ApMentionService,
@@ -71,13 +72,17 @@ export class ApNoteService {
} }
@bindThis @bindThis
public validateNote(object: IObject, uri: string): Error | null { public validateNote(object: IObject, uri: string) {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) {
return new Error('invalid Note: object is null');
}
if (!validPost.includes(getApType(object))) { if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`); return new Error(`invalid Note: invalid object type ${getApType(object)}`);
} }
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
} }
@@ -86,10 +91,10 @@ export class ApNoteService {
if (object.attributedTo && actualHost !== expectHost) { if (object.attributedTo && actualHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
} }
return null; return null;
} }
/** /**
* Noteをフェッチします。 * Noteをフェッチします。
* *
@@ -99,30 +104,31 @@ export class ApNoteService {
public async fetchNote(object: string | IObject): Promise<Note | null> { public async fetchNote(object: string | IObject): Promise<Note | null> {
return await this.apDbResolverService.getNoteFromApId(object); return await this.apDbResolverService.getNoteFromApId(object);
} }
/** /**
* Noteを作成します。 * Noteを作成します。
*/ */
@bindThis @bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> { 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(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
const err = this.validateNote(object, entryUri); const err = this.validateNote(object, entryUri);
if (err) { if (err) {
this.logger.error(err.message, { this.logger.error(`${err.message}`, {
resolver: { history: resolver.getHistory() }, resolver: {
value, history: resolver.getHistory(),
object, },
value: value,
object: object,
}); });
throw new Error('invalid note'); throw new Error('invalid note');
} }
const note = object as IPost; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
if (note.id && !checkHttps(note.id)) { if (note.id && !checkHttps(note.id)) {
@@ -134,25 +140,21 @@ export class ApNoteService {
if (url && !checkHttps(url)) { if (url && !checkHttps(url)) {
throw new Error('unexpected shcema of note url: ' + url); throw new Error('unexpected shcema of note url: ' + url);
} }
this.logger.info(`Creating the Note: ${note.id}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
if (note.attributedTo == null) { const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
let visibility = noteAudience.visibility; let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers; const visibleUsers = noteAudience.visibleUsers;
// Audience (to, cc) が指定されてなかった場合 // Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) { if (visibility === 'specified' && visibleUsers.length === 0) {
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
@@ -160,71 +162,81 @@ export class ApNoteService {
visibility = 'public'; visibility = 'public';
} }
} }
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); 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は必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
const limit = promiseLimit<DriveFile>(2); // Noteがsensitiveなら添付もsensitiveにする
const files = (await Promise.all(toArray(note.attachment).map(attach => ( const limit = promiseLimit(2);
limit(() => this.apImageService.resolveImage(actor, {
...attach, note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする 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 const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver) ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
.then(x => { if (x == null) {
if (x == null) { this.logger.warn('Specified inReplyTo, but not found');
this.logger.warn('Specified inReplyTo, but not found'); throw new Error('inReplyTo not found');
throw new Error('inReplyTo not found'); } else {
}
return x; return x;
}) }
.catch(async err => { }).catch(async err => {
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
throw err; throw err;
}) })
: null; : null;
// 引用 // 引用
let quote: Note | undefined | null = null; let quote: Note | undefined | null;
if (note._misskey_quote || note.quoteUrl) { if (note._misskey_quote || note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise< const tryResolveNote = async (uri: string): Promise<{
| { status: 'ok'; res: Note } status: 'ok';
| { status: 'permerror' | 'temperror' } res: Note | null;
> => { } | {
if (!uri.match(/^https?:/)) return { status: 'permerror' }; status: 'permerror' | 'temperror';
}> => {
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
try { try {
const res = await this.resolveNote(uri); const res = await this.resolveNote(uri);
if (res == null) return { status: 'permerror' }; if (res) {
return { status: 'ok', res }; return {
status: 'ok',
res,
};
} else {
return {
status: 'permerror',
};
}
} catch (e) { } catch (e) {
return { return {
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
}; };
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); 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 (!quote) {
if (results.some(x => x.status === 'temperror')) { if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed'); throw new Error('quote resolve failed');
} }
} }
} }
const cw = note.summary === '' ? null : note.summary; const cw = note.summary === '' ? null : note.summary;
// テキストのパース // テキストのパース
let text: string | null = null; let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
@@ -234,38 +246,38 @@ export class ApNoteService {
} else if (typeof note.content === 'string') { } else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag); text = this.apMfmService.htmlToMfm(note.content, note.tag);
} }
// vote // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => { const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) { } else if (index >= 0) {
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await this.pollService.vote(actor, reply, index); await this.pollService.vote(actor, reply, index);
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(reply.id); this.pollService.deliverQuestionUpdate(reply.id);
} }
return null; return null;
}; };
if (note.name) { if (note.name) {
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
} }
} }
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [] as Emoji[];
}); });
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
return await this.noteCreateService.create(actor, { return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null, createdAt: note.published ? new Date(note.published) : null,
files, files,
@@ -285,7 +297,7 @@ export class ApNoteService {
url: url, url: url,
}, silent); }, silent);
} }
/** /**
* Noteを解決します。 * Noteを解決します。
* *
@@ -296,25 +308,26 @@ export class ApNoteService {
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri'); if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
throw new StatusError('blocked host', 451);
}
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
try { try {
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri); const exist = await this.fetchNote(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
if (uri.startsWith(this.config.url)) { if (uri.startsWith(this.config.url)) {
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
} }
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
@@ -323,61 +336,63 @@ export class ApNoteService {
unlock(); unlock();
} }
} }
@bindThis @bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> { public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
// eslint-disable-next-line no-param-reassign
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji); const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({ const existingEmojis = await this.emojisRepository.findBy({
host, 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 => { return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name.replaceAll(':', ''); const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name); const exists = existingEmojis.find(x => x.name === name);
if (exists) { if (exists) {
if ((exists.updatedAt == null) if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null) || (tag.id != null && exists.uri == null)
|| (new Date(tag.updated) > exists.updatedAt) || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|| (tag.icon.url !== exists.originalUrl) || (tag.icon!.url !== exists.originalUrl)
) { ) {
await this.emojisRepository.update({ await this.emojisRepository.update({
host, host,
name, name,
}, { }, {
uri: tag.id, uri: tag.id,
originalUrl: tag.icon.url, originalUrl: tag.icon!.url,
publicUrl: tag.icon.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
}); });
const emoji = await this.emojisRepository.findOneBy({ host, name }); return await this.emojisRepository.findOneBy({
if (emoji == null) throw new Error('emoji update failed'); host,
return emoji; name,
}) as Emoji;
} }
return exists; return exists;
} }
this.logger.info(`register emoji host=${host}, name=${name}`); this.logger.info(`register emoji host=${host}, name=${name}`);
return await this.emojisRepository.insert({ return await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,
name, name,
uri: tag.id, uri: tag.id,
originalUrl: tag.icon.url, originalUrl: tag.icon!.url,
publicUrl: tag.icon.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
aliases: [], 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 { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; 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 { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } 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 { Note } from '@/models/entities/Note.js';
import type { IdService } from '@/core/IdService.js'; import type { IdService } from '@/core/IdService.js';
import type { MfmService } from '@/core/MfmService.js'; import type { MfmService } from '@/core/MfmService.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -47,8 +48,6 @@ import type { IActor, IObject } from '../type.js';
const nameLength = 128; const nameLength = 128;
const summaryLength = 2048; const summaryLength = 2048;
type Field = Record<'name' | 'value', string>;
@Injectable() @Injectable()
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; private utilityService: UtilityService;
@@ -95,10 +94,28 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: 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.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
@@ -136,6 +153,10 @@ export class ApPersonService implements OnModuleInit {
private validateActor(x: IObject, uri: string): IActor { private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri); const expectHost = this.punyHost(uri);
if (x == null) {
throw new Error('invalid Actor: object is null');
}
if (!isActor(x)) { if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`); throw new Error(`invalid Actor type '${x.type}'`);
} }
@@ -197,19 +218,21 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> { 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; if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(`${this.config.url}/`)) { if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop(); 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); if (u) this.cacheService.uriPersonCache.set(uri, u);
return u; return u;
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null; const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
if (exist) { if (exist) {
this.cacheService.uriPersonCache.set(uri, 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'); 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(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri); const object = await resolver.resolve(uri) as any;
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
const person = this.validateActor(object, uri); const person = this.validateActor(object, uri);
@@ -243,9 +264,9 @@ export class ApPersonService implements OnModuleInit {
const host = this.punyHost(object.id); 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'; const isBot = getApType(object) === 'Service';
@@ -258,7 +279,7 @@ export class ApPersonService implements OnModuleInit {
} }
// Create user // Create user
let user: RemoteUser | null = null; let user: RemoteUser;
try { try {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
@@ -269,16 +290,16 @@ export class ApPersonService implements OnModuleInit {
createdAt: new Date(), createdAt: new Date(),
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
name: truncate(person.name, nameLength), name: truncate(person.name, nameLength),
isLocked: person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo, movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null, movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs, alsoKnownAs: person.alsoKnownAs,
isExplorable: person.discoverable, isExplorable: !!person.discoverable,
username: person.preferredUsername, username: person.preferredUsername,
usernameLower: person.preferredUsername?.toLowerCase(), usernameLower: person.preferredUsername!.toLowerCase(),
host, host,
inbox: person.inbox, 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, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined, featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id, uri: person.id,
@@ -290,9 +311,9 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: user.id, userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url, url: url,
fields, fields,
birthday: bday?.[0] ?? null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
userHost: host, userHost: host,
})); }));
@@ -309,18 +330,21 @@ export class ApPersonService implements OnModuleInit {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await this.usersRepository.findOneBy({ uri: person.id }); const u = await this.usersRepository.findOneBy({
if (u == null) throw new Error('already registered'); uri: person.id,
});
user = u as RemoteUser; if (u) {
user = u as RemoteUser;
} else {
throw new Error('already registered');
}
} else { } else {
this.logger.error(e instanceof Error ? e : new Error(e as string)); this.logger.error(e instanceof Error ? e : new Error(e as string));
throw e; throw e;
} }
} }
if (user == null) throw new Error('failed to create user: user is null');
// Register host // Register host
this.federatedInstanceService.fetch(host).then(async i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); 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 アバターとヘッダー画像をフェッチ //#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => { const [avatar, banner] = await Promise.all([
if (img == null) return null; person.icon,
if (user == null) throw new Error('failed to create user: user is null'); person.image,
return this.apImageService.resolveImage(user, img).catch(() => null); ].map(img =>
})); img == null
? Promise.resolve(null)
: this.apImageService.resolveImage(user!, img).catch(() => null),
));
const avatarId = avatar?.id ?? null; const avatarId = avatar ? avatar.id : null;
const bannerId = banner?.id ?? null; const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar?.blurhash ?? null; const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner?.blurhash ?? null; const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user.id, { await this.usersRepository.update(user!.id, {
avatarId, avatarId,
bannerId, bannerId,
avatarUrl, avatarUrl,
@@ -358,35 +385,37 @@ export class ApPersonService implements OnModuleInit {
bannerBlurhash, bannerBlurhash,
}); });
user.avatarId = avatarId; user!.avatarId = avatarId;
user.bannerId = bannerId; user!.bannerId = bannerId;
user.avatarUrl = avatarUrl; user!.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl; user!.bannerUrl = bannerUrl;
user.avatarBlurhash = avatarBlurhash; user!.avatarBlurhash = avatarBlurhash;
user.bannerBlurhash = bannerBlurhash; user!.bannerBlurhash = bannerBlurhash;
//#endregion //#endregion
//#region カスタム絵文字取得 //#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`); this.logger.info(`extractEmojis: ${err}`);
return []; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user.id, { emojis: emojiNames }); await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion //#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!;
} }
/** /**
* Personの情報を更新します。 * Personの情報を更新します。
* Misskeyに対象のPersonが登録されていなければ無視します。 * Misskeyに対象のPersonが登録されていなければ無視します。
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。 * もしアカウントの移行が確認された場合、アカウント移行処理を行います。
* *
* @param uri URI of Person * @param uri URI of Person
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
@@ -397,14 +426,18 @@ export class ApPersonService implements OnModuleInit {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(`${this.config.url}/`)) return; if (uri.startsWith(`${this.config.url}/`)) {
return;
}
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
if (exist === null) return;
if (exist === null) {
return;
}
//#endregion //#endregion
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri); const object = hint ?? await resolver.resolve(uri);
@@ -414,22 +447,26 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`); this.logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ // アバターとヘッダー画像をフェッチ
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => { const [avatar, banner] = await Promise.all([
if (img == null) return null; person.icon,
return this.apImageService.resolveImage(exist, img).catch(() => null); 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 => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); 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}/); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -442,7 +479,7 @@ export class ApPersonService implements OnModuleInit {
const updates = { const updates = {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
inbox: person.inbox, 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, followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured, featured: person.featured,
emojis: emojiNames, emojis: emojiNames,
@@ -450,29 +487,18 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot: getApType(object) === 'Service', isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null, movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null, alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable, isExplorable: !!person.discoverable,
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; } as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => { const moving =
// 移行先がない→ある // 移行先がない→ある
if ( (!exist.movedToUri && updates.movedToUri) ||
exist.movedToUri === null &&
updates.movedToUri
) return true;
// 移行先がある→別のもの // 移行先がある→別のもの
if ( (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
exist.movedToUri !== null &&
updates.movedToUri !== null &&
exist.movedToUri !== updates.movedToUri
) return true;
// 移行先がある→ない、ない→ないは無視 // 移行先がある→ない、ない→ないは無視
return false;
})();
if (moving) updates.movedAt = new Date(); if (moving) updates.movedAt = new Date();
@@ -499,10 +525,10 @@ export class ApPersonService implements OnModuleInit {
} }
await this.userProfilesRepository.update({ userId: exist.id }, { await this.userProfilesRepository.update({ userId: exist.id }, {
url, url: url,
fields, fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, 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, location: person['vcard:Address'] ?? null,
}); });
@@ -512,10 +538,11 @@ export class ApPersonService implements OnModuleInit {
this.hashtagService.updateUsertags(exist, tags); this.hashtagService.updateUsertags(exist, tags);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await this.followingsRepository.update( await this.followingsRepository.update({
{ followerId: exist.id }, followerId: exist.id,
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, }, {
); followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
});
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
@@ -553,22 +580,27 @@ export class ApPersonService implements OnModuleInit {
*/ */
@bindThis @bindThis
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> { public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchPerson(uri); const exist = await this.fetchPerson(uri);
if (exist) return exist;
if (exist) {
return exist;
}
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
return await this.createPerson(uri, resolver); return await this.createPerson(uri, resolver);
} }
@bindThis @bindThis
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか? public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] { const fields: {
const fields: Field[] = []; name: string,
value: string
}[] = [];
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
fields.push({ fields.push({
@@ -578,11 +610,11 @@ export class ApPersonService implements OnModuleInit {
} }
} }
return fields; return { fields };
} }
@bindThis @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 }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (!this.userEntityService.isRemoteUser(user)) return; if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
@@ -611,13 +643,13 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; 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; td -= 1000;
transactionalEntityManager.insert(UserNotePining, { transactionalEntityManager.insert(UserNotePining, {
id: this.idService.genId(new Date(Date.now() + td)), id: this.idService.genId(new Date(Date.now() + td)),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
noteId: note.id, noteId: note!.id,
}); });
} }
}); });
@@ -656,7 +688,7 @@ export class ApPersonService implements OnModuleInit {
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
dst = await this.resolvePerson(src.movedToUri); dst = await this.resolvePerson(src.movedToUri);
} }
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; //
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; //
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';

View File

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

View File

@@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } 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 []; if (tags == null) return [];
const hashtags = extractApHashtagObjects(tags); const hashtags = extractApHashtagObjects(tags);

View File

@@ -47,7 +47,7 @@ export class DriveFileEntityService {
private videoProcessingService: VideoProcessingService, private videoProcessingService: VideoProcessingService,
) { ) {
} }
@bindThis @bindThis
public validateFileName(name: string): boolean { public validateFileName(name: string): boolean {
return ( return (

View File

@@ -24,7 +24,7 @@ export class NoteEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@@ -68,7 +68,7 @@ export class NoteEntityService implements OnModuleInit {
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService'); this.reactionService = this.moduleRef.get('ReactionService');
} }
@bindThis @bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
@@ -457,12 +457,12 @@ export class NoteEntityService implements OnModuleInit {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId }) .where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId }); .andWhere('note.renoteId = :renoteId', { renoteId });
// 指定した投稿を除く // 指定した投稿を除く
if (excludeNoteId) { if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
} }
return await query.getCount(); return await query.getCount();
} }
} }

View File

@@ -17,7 +17,7 @@ export class NoteReactionEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,

View File

@@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
meId: User['id'], meId: User['id'],
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
options: { options: {
}, },
hint?: { hint?: {
packedNotes: Map<Note['id'], Packed<'Note'>>; packedNotes: Map<Note['id'], Packed<'Note'>>;

View File

@@ -113,7 +113,7 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.pagesRepository) @Inject(DI.pagesRepository)
private pagesRepository: PagesRepository, private pagesRepository: PagesRepository,
@Inject(DI.userMemosRepository) @Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository, private userMemosRepository: UserMemoRepository,

View File

@@ -81,7 +81,7 @@ export class QueueStatsService implements OnApplicationShutdown {
this.intervalId = setInterval(tick, interval); this.intervalId = setInterval(tick, interval);
} }
@bindThis @bindThis
public dispose(): void { public dispose(): void {
clearInterval(this.intervalId); clearInterval(this.intervalId);

View File

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

View File

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

View File

@@ -131,7 +131,7 @@ type NullOrUndefined<p extends Schema, T> =
| T; | T;
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type PartialIntersection<T> = Partial<UnionToIntersection<T>>; type PartialIntersection<T> = Partial<UnionToIntersection<T>>;

View File

@@ -2,7 +2,7 @@
* 1. 配列に何も入っていない時はクエリを付けない * 1. 配列に何も入っていない時はクエリを付けない
* 2. プロパティがundefinedの時はクエリを付けない * 2. プロパティがundefinedの時はクエリを付けない
* new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) * new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
*/ */
export function query(obj: Record<string, unknown>): string { export function query(obj: Record<string, unknown>): string {
const params = Object.entries(obj) const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)

View File

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

View File

@@ -413,16 +413,6 @@ export class Meta {
}) })
public enableChartsForFederatedInstances: boolean; public enableChartsForFederatedInstances: boolean;
@Column('boolean', {
default: false,
})
public enableServerMachineStats: boolean;
@Column('boolean', {
default: true,
})
public enableIdenticonGeneration: boolean;
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })

View File

@@ -207,7 +207,7 @@ export class UserProfile {
public mutedInstances: string[]; public mutedInstances: string[];
@Column('enum', { @Column('enum', {
enum: [ enum: [
...notificationTypes, ...notificationTypes,
// マイグレーションで削除が困難なので古いenumは残しておく // マイグレーションで削除が困難なので古いenumは残しておく
...obsoleteNotificationTypes, ...obsoleteNotificationTypes,

View File

@@ -283,7 +283,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
}); });
const relationshipLogger = this.logger.createSubLogger('relationship'); const relationshipLogger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorker this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))

View File

@@ -30,7 +30,7 @@ export class ExportAntennasProcessorService {
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListJoiningsRepository: UserListJoiningsRepository,
private driveService: DriveService, private driveService: DriveService,
private utilityService: UtilityService, private utilityService: UtilityService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,

View File

@@ -17,11 +17,11 @@ const validate = new Ajv().compile({
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
userListAccts: { userListAccts: {
type: 'array', type: 'array',
items: { items: {
type: 'string', type: 'string',
}, },
nullable: true, nullable: true,
}, },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {

View File

@@ -113,7 +113,7 @@ export class ImportCustomEmojisProcessorService {
} }
cleanup(); cleanup();
this.logger.succ('Imported'); this.logger.succ('Imported');
}); });
unzipStream.pipe(extractor); unzipStream.pipe(extractor);

View File

@@ -31,7 +31,7 @@ export class WebhookDeliverProcessorService {
public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> { public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> {
try { try {
this.logger.debug(`delivering ${job.data.webhookId}`); this.logger.debug(`delivering ${job.data.webhookId}`);
const res = await this.httpRequestService.send(job.data.to, { const res = await this.httpRequestService.send(job.data.to, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -50,25 +50,25 @@ export class WebhookDeliverProcessorService {
body: job.data.content, body: job.data.content,
}), }),
}); });
this.webhooksRepository.update({ id: job.data.webhookId }, { this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(), latestSentAt: new Date(),
latestStatus: res.status, latestStatus: res.status,
}); });
return 'Success'; return 'Success';
} catch (res) { } catch (res) {
this.webhooksRepository.update({ id: job.data.webhookId }, { this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(), latestSentAt: new Date(),
latestStatus: res instanceof StatusError ? res.statusCode : 1, latestStatus: res instanceof StatusError ? res.statusCode : 1,
}); });
if (res instanceof StatusError) { if (res instanceof StatusError) {
// 4xx // 4xx
if (res.isClientError) { if (res.isClientError) {
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
} }
// 5xx etc. // 5xx etc.
throw new Error(`${res.statusCode} ${res.statusMessage}`); throw new Error(`${res.statusCode} ${res.statusMessage}`);
} else { } else {

View File

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

View File

@@ -36,6 +36,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js'; import { ClientLoggerService } from './web/ClientLoggerService.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@Module({ @Module({
imports: [ imports: [
@@ -78,6 +79,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.
ServerStatsChannelService, ServerStatsChannelService,
UserListChannelService, UserListChannelService,
OpenApiServerService, OpenApiServerService,
OAuth2ProviderService,
], ],
exports: [ exports: [
ServerService, ServerService,

View File

@@ -16,7 +16,6 @@ import { createTemp } from '@/misc/create-temp.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js'; import { ApiServerService } from './api/ApiServerService.js';
@@ -25,6 +24,7 @@ import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url)); const _dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -46,7 +46,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private apiServerService: ApiServerService, private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService, private openApiServerService: OpenApiServerService,
@@ -58,12 +57,13 @@ export class ServerService implements OnApplicationShutdown {
private clientServerService: ClientServerService, private clientServerService: ClientServerService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private loggerService: LoggerService, private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
) { ) {
this.logger = this.loggerService.getLogger('server', 'gray', false); this.logger = this.loggerService.getLogger('server', 'gray', false);
} }
@bindThis @bindThis
public async launch() { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true, trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
@@ -92,6 +92,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.activityPubServerService.createServer); fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer); fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer);
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;
@@ -163,16 +164,11 @@ export class ServerService implements OnApplicationShutdown {
}); });
fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => { fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => {
const [temp, cleanup] = await createTemp();
await genIdenticon(request.params.x, fs.createWriteStream(temp));
reply.header('Content-Type', 'image/png'); reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
return fs.createReadStream(temp).on('close', () => cleanup());
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
const [temp, cleanup] = await createTemp();
await genIdenticon(request.params.x, fs.createWriteStream(temp));
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) => { fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {
@@ -231,7 +227,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis @bindThis
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
await this.streamingApiServerService.detach(); await this.streamingApiServerService.detach();
await this.#fastify.close(); await this.#fastify.close();
} }

View File

@@ -40,15 +40,15 @@ export class AuthenticateService implements OnApplicationShutdown {
if (token == null) { if (token == null) {
return [null, null]; return [null, null];
} }
if (isNativeToken(token)) { if (isNativeToken(token)) {
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>); () => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
if (user == null) { if (user == null) {
throw new AuthenticationError('user not found'); throw new AuthenticationError('user not found');
} }
return [user, null]; return [user, null];
} else { } else {
const accessToken = await this.accessTokensRepository.findOne({ const accessToken = await this.accessTokensRepository.findOne({
@@ -58,24 +58,24 @@ export class AuthenticateService implements OnApplicationShutdown {
token: token, // miauth token: token, // miauth
}], }],
}); });
if (accessToken == null) { if (accessToken == null) {
throw new AuthenticationError('invalid signature'); throw new AuthenticationError('invalid signature');
} }
this.accessTokensRepository.update(accessToken.id, { this.accessTokensRepository.update(accessToken.id, {
lastUsedAt: new Date(), lastUsedAt: new Date(),
}); });
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId, const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
() => this.usersRepository.findOneBy({ () => this.usersRepository.findOneBy({
id: accessToken.userId, id: accessToken.userId,
}) as Promise<LocalUser>); }) as Promise<LocalUser>);
if (accessToken.appId) { if (accessToken.appId) {
const app = await this.appCache.fetch(accessToken.appId, const app = await this.appCache.fetch(accessToken.appId,
() => this.appsRepository.findOneByOrFail({ id: accessToken.appId! })); () => this.appsRepository.findOneByOrFail({ id: accessToken.appId! }));
return [user, { return [user, {
id: accessToken.id, id: accessToken.id,
permission: app.permission, permission: app.permission,

View File

@@ -38,14 +38,14 @@ export class RateLimiterService {
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
minIntervalLimiter.get((err, info) => { minIntervalLimiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject('ERR');
} }
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL'); reject('BRIEF_REQUEST_INTERVAL');
} else { } else {
@@ -57,7 +57,7 @@ export class RateLimiterService {
} }
}); });
}; };
// Long term limit // Long term limit
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
@@ -66,14 +66,14 @@ export class RateLimiterService {
max: limitation.max! / factor, max: limitation.max! / factor,
db: this.redisClient, db: this.redisClient,
}); });
limiter.get((err, info) => { limiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject('ERR');
} }
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED'); reject('RATE_LIMIT_EXCEEDED');
} else { } else {
@@ -81,13 +81,13 @@ export class RateLimiterService {
} }
}); });
}; };
const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit = const hasLongTermLimit =
typeof limitation.duration === 'number' && typeof limitation.duration === 'number' &&
typeof limitation.max === 'number'; typeof limitation.max === 'number';
if (hasShortTermLimit) { if (hasShortTermLimit) {
min(); min();
} else if (hasLongTermLimit) { } else if (hasLongTermLimit) {

View File

@@ -36,7 +36,7 @@ export class SigninService {
headers: request.headers as any, headers: request.headers as any,
success: true, success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
// Publish signin event // Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
}); });

View File

@@ -103,13 +103,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: { this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
stream: MainStreamConnection, stream: MainStreamConnection,
user: LocalUser | null; user: LocalUser | null;
@@ -119,11 +112,12 @@ export class StreamingApiServerService {
const ev = new EventEmitter(); const ev = new EventEmitter();
function onRedisMessage(data: any): void { async function onRedisMessage(_: string, data: string): Promise<void> {
ev.emit(data.channel, data.message); 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); await stream.listen(ev, connection);
@@ -143,7 +137,7 @@ export class StreamingApiServerService {
connection.once('close', () => { connection.once('close', () => {
ev.removeAllListeners(); ev.removeAllListeners();
stream.dispose(); stream.dispose();
globalEv.off('message', onRedisMessage); this.redisForSub.off('message', onRedisMessage);
this.#connections.delete(connection); this.#connections.delete(connection);
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
}); });

View File

@@ -34,23 +34,23 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => { this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined; let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) { if (meta.requireFile) {
cleanup = () => { cleanup = () => {
if (file) fs.unlink(file.path, () => {}); if (file) fs.unlink(file.path, () => {});
}; };
if (file == null) return Promise.reject(new ApiError({ if (file == null) return Promise.reject(new ApiError({
message: 'File required.', message: 'File required.',
code: 'FILE_REQUIRED', code: 'FILE_REQUIRED',
id: '4267801e-70d1-416a-b011-4ee502885d8b', id: '4267801e-70d1-416a-b011-4ee502885d8b',
})); }));
} }
const valid = validate(params); const valid = validate(params);
if (!valid) { if (!valid) {
if (file) cleanup!(); if (file) cleanup!();
const errors = validate.errors!; const errors = validate.errors!;
const err = new ApiError({ const err = new ApiError({
message: 'Invalid param.', message: 'Invalid param.',
@@ -62,7 +62,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
}); });
return Promise.reject(err); return Promise.reject(err);
} }
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers); return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
}; };
} }

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; 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); return await this.abuseUserReportEntityService.packMany(reports);
}); });

View File

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

View File

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

View File

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

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