Compare commits

...

47 Commits

Author SHA1 Message Date
syuilo
0fbb46c981 13.14.0-beta.1 2023-07-08 16:53:46 +09:00
syuilo
081a14d6f3 perf(backend): use limit() instead of take() 2023-07-08 16:53:07 +09:00
syuilo
b056e8f5eb use node 20.3.1
Fix #11179
2023-07-08 15:48:46 +09:00
syuilo
15683370f0 fix(frontend): ページ遷移でスクロール位置が保持されない問題を修正
Fix #11068
2023-07-08 15:30:36 +09:00
syuilo
644023316e refactor: use esm 2023-07-08 13:03:31 +09:00
syuilo
c2d7008cff tweak localization
Resolve #11119
2023-07-08 12:53:51 +09:00
Chocolate Pie
bd843863d0 fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 (#10973)
* 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 (misskey-dev/misskey#10922)

* Update CHANGELOG.md

* fix

* Update CHANGELOG.md

* Update CHANGELOG.md
2023-07-08 08:58:35 +09:00
CyberRex
8ec96ad1e0 fix(backend): ジョブキュー再試行時のタイミングずれによるエラーを抑制 (#11035)
* fix(backend): ジョブキュー再試行時のタイミングずれによるエラーを抑制

* fix lint
2023-07-08 08:57:23 +09:00
okayurisotto
4f876c9e8d refactor(backend): core/activitypub/models (#11067)
* cleanup(`ApImageService.ts`)

* refactor(`ApImageService.ts`)

* cleanup(`check-https.ts`)

* cleanup(`ApMentionService.ts`)

* refactor(`ApMentionService.ts`)

* cleanup(`ApNoteService.ts`): unneeded `eslint-disable-next-line`

* cleanup(`ApNoteService.ts`)

* WIP(`ApImageService.ts`): `image.url`を`getApHrefNullable()`に通すかどうか悩んでいる

* refactor(`ApNoteService.ts`): function return type

* cleanup(`ApNoteService.ts`): deadcode

* refactor(`ApNoteService.ts`): `eslint-disable-next-line`

* refactor(`ApNoteService.ts`): non-null assertion

これまでは`getApId()`の方でエラーがスローされていた。

* cleanup(`ApNoteService.ts`): unneeded await

* refactor(`ApNoteService.ts`): note.attachment

- `toArray()`を使うように
- よくわからない条件式を整理
- `as`をなくすために`promiseLimit()`でジェネリクスを使うように

* cleanup(`ApNoteService.ts`)

* refactor(`ApNoteService.ts`): よりよい型定義

`res`が`null`でないことは確認されているようだったので`null`とのunionはなくした

* refactor(`ApNoteService.ts`): 不要な条件を削除

* cleanup(`ApNoteService.ts`)

* cleanup(`ApNoteService.ts`): 重要でない`as`を削除

* refactor(`ApNoteService.ts`): `eslint-disable-next-line`

* cleanup(`ApNoteService.ts`): deadcode

* cleanup(`ApNoteService.ts`): unneeded non-null assertion

* refactor(`ApNoteService.ts`): 不要な条件を削除

* WIP(`ApNoteService.ts`): `as`をなくす

エラーメッセージを考える

* cleanup(`ApNoteService.ts`): 不要な`as`を削除

* cleanup(`ApPersonService.ts`): `no-unused-vars`

* cleanup(`ApPersonService.ts`): deadcode

* refactor(`ApPersonService.ts`): function return type

* cleanup(`ApPersonService.ts`): deadcode

* cleanup(`ApPersonService.ts`): deadcode

* WIP(`ApPersonService.ts`): `as`を調整

`null`でないか確認する処理が続いていたので型アサーションは`null`とのunionにした。
より本質的な改善の余地があるように感じるのでひとまずWIPとしてコミット。

* refactor(`ApPersonService.ts`): `eslint-disable-next-line`

* WIP(`ApPersonService.ts`): `as any`をなくした

エラーをスローするようにせざるを得なかったのでエラーメッセージを考える必要がある。

* WIP(`ApNoteService.ts`): non-null assertion

non-nullアサーションを減らすために事前に存在確認をするようにした。
エラーをスローするようにしたのでメッセージを考えなければならない。

* refactor(`ApNoteService.ts`): non-null assertion -> optional chaining

* refactor(`ApPersonService.ts`): `eslint-disable-next-line`

* refactor(`ApPersonService.ts`): `eslint-disable-next-line`

* refactor(`ApPersonService.ts`): function return type

* refactor(`ApPersonService.ts`): type guardによるnon-null assertionの削除

* WIP(`ApPersonService.ts`): `analyzeAttachments`

- Field型を事前に定義しておくように

- `attachments`が`IObject`だった場合、返り値が`{ fields: [] }`になるようだが構わないのか?
- `toArray()`を通すべきでは?

* Revert "WIP(`ApImageService.ts`): `image.url`を`getApHrefNullable()`に通すかどうか悩んでいる"

This reverts commit aeefb843a8.

* cleanup(`ApImageService.ts`): `import`

* refactor(`ApImageService.ts`): 冗長だった部分を短く

* cleanup(`ApMentionService.ts`): `import`

* refactor(`ApImageService.ts`): `JSON.stringify()`でのindentationを追加

* cleanup(`ApNoteService.ts`): `import`

* cleanup(`ApNoteService.ts`)

* cleanup(`ApNoteService.ts`)

* cleanup(`ApNoteService.ts`)

* cleanup(`ApNoteService.ts`): `any`に対するnon-null assertion

* refactor(`ApNoteService.ts`): 添付ファイル

* cleanup(`ApPersonService.ts`): `import`

* refactor(`ApPersonService.ts`): より実情に即した`as`に

* cleanup(`ApPersonService.ts`)

* refactor(`ApPersonService.ts`): 冗長だった部分を修正

* cleanup(`ApPersonService.ts`): deadcode

* cleanup(`ApPersonService.ts`)

* cleanup(`ApQuestionService.ts`): `import`

* refactor(`ApQuestionService.ts`): `eslint-disable-next-line`

* refactor(`ApQuestionService.ts`): `eslint-disable-next-line`

* cleanup(`ApQuestionService.ts`)

* refactor(`ApQuestionService.ts`): non-null assertionを消した

* cleanup(`ApQuestionService.ts`)

* WIP(`ApQuestionService.ts`): non-null assertionを消す

エラーメッセージを考える必要がある。

* refactor(`ApQuestionService.ts`): `any`を消す

* refactor(`ApQuestionService.ts`): function return type

* WIP(`ApPersonService.ts`): 可読性の低い三項演算子を削除しつつnon-null assertionを回避

エラーメッセージを考える必要がある。

* cleanup(`ApPersonService.ts`): 不必要な三項演算子を削除

* cleanup(`ApPersonService.ts`): 不要な`as`

* cleanup(`ApPersonService.ts`)

* refactor(`ApPersonService.ts`)

* refactor(`ApPersonService.ts`): 可読性の低い三項演算子を削除

元の実装が悪いと判断し`null`かどうかの確認をより厳密に行うようにした。

* cleanup(`ApPersonService.ts`)

* cleanup(`ApPersonService.ts`)

* refactor(`ApPersonService.ts`): 返り値を`void`に統一

この返り値を参照しているコードは見当たらなかった。
また、普通に意味がない値であるように見受けられた。

* fixup! refactor(`ApPersonService.ts`): 返り値を`void`に統一

* refactor(`ApNoteService.ts`)

* refactor(`ApPersonService.ts`)

* cleanup(`ApPersonService.ts`)

* cleanup(`ApPersonService.ts`)

* refactor(`ApPersonService.ts`): 返り値の`void`統一と条件式の調整

この返り値を参照しているコードは見当たらなかった。
また、普通に意味がない値であるように見受けられた。

* cleanup(`ApQuestionService.ts`)

* refactor(`ApQuestionService.ts`)

* refactor(`ApQuestionService.ts`)

* refactor(`tag.ts`): function return type

* fixup! enhance: account migration (#10592)

* fixup! WIP(`ApPersonService.ts`): 可読性の低い三項演算子を削除しつつnon-null assertionを回避

* fixup! cleanup(`ApPersonService.ts`): 不要な`as`

* refactor: エラーメッセージを見繕った

* Revert "cleanup(`ApImageService.ts`): `import`"

This reverts commit 1454d04c37.

* Revert "cleanup(`ApMentionService.ts`): `import`"

This reverts commit 244f6720c1.

* Revert "cleanup(`ApNoteService.ts`): `import`"

This reverts commit d8f0d76973.

* Revert "cleanup(`ApPersonService.ts`): `import`"

This reverts commit 5190ef954c.

# Conflicts:
#	packages/backend/src/core/activitypub/models/ApPersonService.ts

* Revert "cleanup(`ApQuestionService.ts`): `import`"

This reverts commit 778585e288.

* processRemoteMoveはそのままにしてほしい

* Revert "fixup! refactor(`ApPersonService.ts`): 返り値を`void`に統一"

This reverts commit 083cd678ab.

* Revert "refactor(`ApPersonService.ts`): 返り値を`void`に統一"

This reverts commit bfa0fcd6f0.

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-08 08:57:13 +09:00
nenohi
3c6175d959 広告の曜日を設定できるように (#10095)
* 曜日選択できるように

* ラベル選択でもチェックが変更されるように

* adを参照しないといけないかも

* smallint -> integer

* 異物混入だったので取りだし

* タイムゾーン指定(Date2つ使うのなんか違和感

* 未テスト

* これにすると出てこないかも

* UIチョット変更

* UI変更 fix bug

* 畳むように修正

* dayofweek->dayOfWeek

* マイグレ時にnot null,default設定してるのでnullable:falseでよさそう

* コメントの記載

* Update packages/backend/src/server/api/endpoints/meta.ts

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

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-07-08 08:56:11 +09:00
syuilo
1f181536ae use engines 2023-07-08 08:52:51 +09:00
syuilo
383d6a2485 nodeの推奨(デフォルト)バージョンと最小バージョンを分離 2023-07-08 08:50:02 +09:00
syuilo
588465566b 🎨 2023-07-08 08:46:42 +09:00
tamaina
b318789354 fix(backend): deliverManyにcontentのnullチェックを追加
https://github.com/MisskeyIO/misskey/pull/99
2023-07-07 23:15:04 +00:00
tamaina
0b8e0fa91b fix 2023-07-07 22:55:53 +00:00
popkirby
8daca59ca6 perf(backend): use mutex for nsfw model loading (#11109)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-08 07:27:26 +09:00
okayurisotto
d84796588c cleanup: trim trailing whitespace (#11136)
* cleanup: trim trailing whitespace

* update(`.editorconfig`)

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-07-08 07:08:16 +09:00
Yuriha
4c879b3a33 perf(backend): Improve performance of FetchInstanceMetadata (#11128)
* Perf: Avoid retries to acquire lock in fetchInstanceMetadata

* Fix

* Add Changelog

* Fix typo

* Fix lint

* 記法をMisskey式にする

* ????

* refactor
https://github.com/misskey-dev/misskey/pull/11128#pullrequestreview-1518059366

* refactor

* getいらない?

* fix

* fix

* Update CHANGELOG.md

* clean up

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-07 23:28:27 +09:00
syuilo
eacc90debc fix(client): ZenUIでポップアップの表示位置がおかしい問題を修正 2023-07-07 20:18:06 +09:00
anatawa12
2606167f0d chore: collapse renote of my note (#11166)
* chore(frontend): 自分のnoteのrenoteも省略するように

Co-authored-by: madorama <madorama999@gmail.com>

* docs(changelog): add 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように

---------

Co-authored-by: madorama <madorama999@gmail.com>
2023-07-07 20:05:11 +09:00
syuilo
f76b3edbdd update node to 20.4.0 2023-07-07 13:58:43 +09:00
tamaina
aef7b0238b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-07-07 04:51:04 +00:00
tamaina
cbb58b1cfc update changelog 2023-07-07 04:50:56 +00:00
Narazaka
bc4d27410c feat: webp convert @frontend (#11150)
* webp convert @frontend

* 0.85 → 0.90

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-07 13:44:31 +09:00
syuilo
d5c4e77c44 update deps 2023-07-07 10:53:06 +09:00
syuilo
e987af4e4f Update .gitignore 2023-07-07 10:49:17 +09:00
Ryoh827
bc61f37faa refactor(frontend): fix enum types in scripts/form (#11138) 2023-07-06 20:23:54 +09:00
syuilo
c065b97140 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-07-06 16:18:09 +09:00
syuilo
0137af892a chore(frontend): tweak photoswipe animation
Resolve #11117
2023-07-06 16:18:06 +09:00
tamaina
06bf5c1ff1 fix(frontend): In MkPagination, init() also initializes items
ユーザーページのノートタブで小タブを変更すると前のタイムラインが残る問題を修正
2023-07-06 06:43:05 +00:00
syuilo
9e955d20c4 🎨 2023-07-06 15:07:51 +09:00
syuilo
165c53a547 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-07-06 15:04:42 +09:00
syuilo
3597da5c49 Update about-misskey.vue 2023-07-06 15:04:39 +09:00
okayurisotto
4a7da723b3 refactor(backend): ノート削除時のfindCascadingNotesの処理を整理 (#11131)
* refactor(backend): ノート削除時の`findCascadingNotes`の処理を整理

* cleanup: unneeded async await

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

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-07-06 11:25:46 +09:00
EdamAme
d2f8ed95aa エスケープせずにDescriptionを出力、Update info-card.pug (#11108)
HTMLのタグがエスケープされ、
misskey-hub.netのサーバー一覧で、iframeで読み込む際にタグがそのまま出力される状況が発生していた。
pugにおける仕様に則り、!=に変更、エスケープを行わないように。
2023-07-06 09:42:57 +09:00
Ikko Eltociear Ashimine
6b2c92cb68 chore(backend): fix typo in MkImgWithBlurhash.vue (#11125)
occured -> occurred
2023-07-06 09:19:10 +09:00
anatawa12
dc8763215a feat(frontend): 画像を動画と同様に簡単に隠せるように (#11127)
* feat: hide image easily

* docs(changelog): add 画像を動画と同様に簡単に隠せるように
2023-07-06 08:49:07 +09:00
okayurisotto
9959f5bd04 refactor(ApDbResolverService.ts): URLを扱う複雑な正規表現をURLインターフェイスで置き換え (#11123)
* refactor(`ApDbResolverService.ts`): URLを扱う複雑な正規表現をURLインターフェイスで置き換え

* fixup! refactor(`ApDbResolverService.ts`): URLを扱う複雑な正規表現をURLインターフェイスで置き換え
2023-07-06 08:47:47 +09:00
tamaina
be143f91b2 update CHANGELOG.md 2023-07-05 04:57:19 +00:00
Kagami Sascha Rosylight
ac4245dce1 feat(frontend): allow cropping images on drive (#11092)
* feat(frontend): allow cropping images on drive

* nanka iroiro

* folder

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-05 13:54:40 +09:00
anatawa12
1ab9f096c3 feat(frontend): deck UIのカラムからアンテナ、リストの編集画面を開けるように (#11104)
* feat: add edit antenna button onto deck column

* feat: add edit list button onto deck column

* docs(changelog): add deck UIのカラムのメニューからアンテナとリストの編集画面を開けるようになりました
2023-07-05 13:04:27 +09:00
Umisyo(Souta Kusunoki)
8f94b36732 refactor: ApDeliverManagerService.tsの型とJSDocを適切に置き換え (#11096)
* refactor: ApDeliverManagerService.ts のanyを適切な型に置き換え

Signed-off-by: Umisyo <kusunokisouta@gmail.com>

* fix: quote to single quote

Signed-off-by: Umisyo <kusunokisouta@gmail.com>

* refactor: JSDocを実態に合わせて修正

Signed-off-by: Umisyo <kusunokisouta@gmail.com>

* fix: activityのnullを許容するよう変更

Signed-off-by: Umisyo <kusunokisouta@gmail.com>

---------

Signed-off-by: Umisyo <kusunokisouta@gmail.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-05 12:17:52 +09:00
tamaina
22227fa641 perf(backend): Use addBulk to add deliver queues (#11114) 2023-07-05 12:15:48 +09:00
tamaina
92d9946f59 enhance(frontend): Better Timeline(MkPagination) Experience (#11066)
* enhance(frontend): Better MkPagination Appearance

* fix

* fix

* 新規投稿が空でも先頭に戻ったらunshiftItemsする

* use Map

* refactor, 型エラー潰し

* refactor
2023-07-05 00:59:37 +09:00
tamaina
526fa8bf3f perf(frontend): use setInterval instead of setTimeout chain in MkTime (#10981)
* perf(frontend): use setInterval instead of setTimeout chain in MkTime

* fix

* props.origin

* props.origin 2

* fix

* add comment

* setIntervalを再設定する

* refactor
2023-07-04 22:48:39 +09:00
tamaina
aa92df4e50 chore(frontend): add comment 2023-07-04 11:21:44 +00:00
riku6460
61e7eb8ff1 perf(backend): JSON.parse の呼び出しを削減する (#11091)
* perf(backend): JSON.parse の呼び出しを削減する

Co-authored-by: Hidekazu Kobayashi <kobahide789@gmail.com>

* Update CHANGELOG.md

---------

Co-authored-by: Hidekazu Kobayashi <kobahide789@gmail.com>
2023-07-04 07:49:13 +09:00
289 changed files with 4133 additions and 3441 deletions

View File

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

View File

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

View File

@@ -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 -->
* Misskey: 13.x.x
* Node: 18.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View File

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

View File

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

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

@@ -1 +1 @@
18.16.0
20.3.1

View File

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

View File

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

8
locales/index.d.ts vendored
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
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

@@ -3,6 +3,9 @@
"main": "./index.js",
"private": true,
"type": "module",
"engines": {
"node": ">=18.16.0"
},
"scripts": {
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js",
@@ -51,12 +54,12 @@
"utf-8-validate": "^6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.321.1",
"@aws-sdk/lib-storage": "3.321.1",
"@aws-sdk/node-http-handler": "3.321.1",
"@bull-board/api": "5.5.3",
"@bull-board/fastify": "5.5.3",
"@bull-board/ui": "5.5.3",
"@aws-sdk/client-s3": "3.367.0",
"@aws-sdk/lib-storage": "3.367.0",
"@aws-sdk/node-http-handler": "3.360.0",
"@bull-board/api": "5.6.0",
"@bull-board/fastify": "5.6.0",
"@bull-board/ui": "5.6.0",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0",
@@ -64,21 +67,22 @@
"@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.0",
"@fastify/static": "6.10.2",
"@fastify/view": "7.4.1",
"@nestjs/common": "10.0.3",
"@nestjs/core": "10.0.3",
"@nestjs/testing": "10.0.3",
"@fastify/view": "8.0.0",
"@nestjs/common": "10.0.5",
"@nestjs/core": "10.0.5",
"@nestjs/testing": "10.0.5",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.66",
"@swc/core": "1.3.68",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"async-mutex": "^0.4.0",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bullmq": "4.1.0",
"bullmq": "4.2.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.0",
"chalk": "5.2.0",
@@ -90,18 +94,18 @@
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.18.0",
"fastify": "4.19.2",
"feed": "4.2.2",
"file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "13.0.0",
"happy-dom": "9.20.3",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0",
"is-svg": "4.3.2",
"is-svg": "5.0.0",
"js-yaml": "4.1.0",
"jsdom": "22.1.0",
"json5": "2.2.3",
@@ -118,9 +122,9 @@
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "9.1.2",
"otpauth": "9.1.3",
"parse5": "7.1.2",
"pg": "8.11.0",
"pg": "8.11.1",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
@@ -144,14 +148,14 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.4",
"systeminformation": "5.18.6",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.6",
"tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
"typescript": "5.1.3",
"typescript": "5.1.6",
"ulid": "2.3.0",
"unzipper": "0.10.14",
"uuid": "9.0.0",
@@ -161,7 +165,7 @@
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.5.0",
"@jest/globals": "29.6.1",
"@swc/jest": "0.2.26",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
@@ -178,14 +182,14 @@
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31",
"@types/node": "20.3.1",
"@types/node": "20.4.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1",
"@types/pg": "8.10.2",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.0",
"@types/qrcode": "1.5.1",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
@@ -202,14 +206,14 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.60.0",
"aws-sdk-client-mock": "2.1.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.43.0",
"eslint": "8.44.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",
"jest-mock": "29.5.0"
"execa": "7.1.1",
"jest": "29.6.1",
"jest-mock": "29.6.1"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,10 +121,8 @@ export class NoteDeleteService {
}
@bindThis
private async findCascadingNotes(note: Note) {
const cascadingNotes: Note[] = [];
const recursive = async (noteId: string) => {
private async findCascadingNotes(note: Note): Promise<Note[]> {
const recursive = async (noteId: string): Promise<Note[]> => {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
@@ -133,12 +131,14 @@ export class NoteDeleteService {
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
for (const reply of replies) {
cascadingNotes.push(reply);
await recursive(reply.id);
}
return [
replies,
...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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.andWhere('folder.parentId IS NULL');
}
const folders = await query.take(ps.limit).getMany();
const folders = await query.limit(ps.limit).getMany();
return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder)));
});

View File

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

View File

@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('following.followeeHost = :host', { host: ps.host });
const followings = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });

View File

@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('following.followerHost = :host', { host: ps.host });
const followings = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });

View File

@@ -126,7 +126,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' });
}
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
const instances = await query.limit(ps.limit).skip(ps.offset).getMany();
return await this.instanceEntityService.packMany(instances);
});

View File

@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('user.host = :host', { host: ps.host });
const users = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.userEntityService.packMany(users, me, { detail: true });

View File

@@ -40,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('flash.likedCount > 0')
.orderBy('flash.likedCount', 'DESC');
const flashs = await query.take(10).getMany();
const flashs = await query.limit(10).getMany();
return await this.flashEntityService.packMany(flashs, me);
});

View File

@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('like.flash', 'flash');
const likes = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return this.flashLikeEntityService.packMany(likes, me);

View File

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

View File

@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('request.followeeId = :meId', { meId: me.id });
const requests = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));

View File

@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('post.likedCount > 0')
.orderBy('post.likedCount', 'DESC');
const posts = await query.take(10).getMany();
const posts = await query.limit(10).getMany();
return await this.galleryPostEntityService.packMany(posts, me);
});

View File

@@ -40,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('post.likedCount > 0')
.orderBy('post.likedCount', 'DESC');
const posts = await query.take(10).getMany();
const posts = await query.limit(10).getMany();
return await this.galleryPostEntityService.packMany(posts, me);
});

View File

@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId)
.innerJoinAndSelect('post.user', 'user');
const posts = await query.take(ps.limit).getMany();
const posts = await query.limit(ps.limit).getMany();
return await this.galleryPostEntityService.packMany(posts, me);
});

View File

@@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
'tag.attachedRemoteUsersCount',
]);
const tags = await query.take(ps.limit).getMany();
const tags = await query.limit(ps.limit).getMany();
return this.hashtagEntityService.packMany(tags);
});

View File

@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
.orderBy('tag.count', 'DESC')
.groupBy('tag.id')
.take(ps.limit)
.limit(ps.limit)
.skip(ps.offset)
.getMany();

View File

@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
}
const users = await query.take(ps.limit).getMany();
const users = await query.limit(ps.limit).getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
});

View File

@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('favorite.note', 'note');
const favorites = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.noteFavoriteEntityService.packMany(favorites, me);

View File

@@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('like.post', 'post');
const likes = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.galleryLikeEntityService.packMany(likes, me);

View File

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

View File

@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('like.page', 'page');
const likes = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return this.pageLikeEntityService.packMany(likes, me);

View File

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

View File

@@ -35,7 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
.andWhere('signin.userId = :meId', { meId: me.id });
const history = await query.take(ps.limit).getMany();
const history = await query.limit(ps.limit).getMany();
return await Promise.all(history.map(record => this.signinEntityService.pack(record)));
});

View File

@@ -1,4 +1,4 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js';
@@ -263,12 +263,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
const ads = await this.adsRepository.find({
where: {
expiresAt: MoreThan(new Date()),
startsAt: LessThanOrEqual(new Date()),
},
});
const ads = await this.adsRepository.createQueryBuilder("ads")
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const response: any = {
maintainerName: instance.maintainerName,
@@ -311,6 +314,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let notes = await query
.orderBy('note.score', 'DESC')
.take(100)
.limit(100)
.getMany();
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

View File

@@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {

View File

@@ -137,7 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);

View File

@@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {

View File

@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.setParameters(followingQuery.getParameters());
}
const mentions = await query.take(ps.limit).getMany();
const mentions = await query.limit(ps.limit).getMany();
this.noteReadService.read(me.id, mentions);

View File

@@ -82,7 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const polls = await query
.orderBy('poll.noteId', 'DESC')
.take(ps.limit)
.limit(ps.limit)
.skip(ps.offset)
.getMany();

View File

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

View File

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

View File

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

View File

@@ -123,7 +123,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);

View File

@@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.limit(ps.limit).getMany();
this.activeUsersChart.read(me);

View File

@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('page.likedCount > 0')
.orderBy('page.likedCount', 'DESC');
const pages = await query.take(10).getMany();
const pages = await query.limit(10).getMany();
return await this.pageEntityService.packMany(pages, me);
});

View File

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

View File

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

View File

@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me) this.queryService.generateMutedUserQueryForUsers(query, me);
if (me) this.queryService.generateBlockQueryForUsers(query, me);
query.take(ps.limit);
query.limit(ps.limit);
query.skip(ps.offset);
const users = await query.getMany();

View File

@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('clip.isPublic = true');
const clips = await query
.take(ps.limit)
.limit(ps.limit)
.getMany();
return await this.clipEntityService.packMany(clips, me);

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