Compare commits
22 Commits
13.14.0-be
...
13.13.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
407a965c1d | ||
![]() |
de6348e8a0 | ||
![]() |
9ad57324db | ||
![]() |
94690c835e | ||
![]() |
c5d2dba28d | ||
![]() |
272e0c874f | ||
![]() |
d429f810a9 | ||
![]() |
75b28d6782 | ||
![]() |
8b1362ab03 | ||
![]() |
a096f621cf | ||
![]() |
f54a9542bb | ||
![]() |
a52bbc7c8d | ||
![]() |
59768bdf3f | ||
![]() |
1e67e9c661 | ||
![]() |
ae517a99a7 | ||
![]() |
b23a9b1a88 | ||
![]() |
5bd68aa3e0 | ||
![]() |
647ce174b3 | ||
![]() |
02c8fd9de5 | ||
![]() |
1ba49b614d | ||
![]() |
40de14415c | ||
![]() |
7c9330a02f |
@@ -6,7 +6,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20.3.1"
|
"version": "18.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
|
@@ -2,7 +2,7 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
@@ -6,10 +6,6 @@ indent_size = 2
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
24
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
24
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
@@ -39,22 +39,8 @@ Please include errors from the developer console and/or server log files if you
|
|||||||
<!-- Tell us where on the platform it happens -->
|
<!-- Tell us where on the platform it happens -->
|
||||||
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
|
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
|
||||||
|
|
||||||
### 💻 Frontend
|
Misskey version:
|
||||||
* Model and OS of the device(s):
|
PostgreSQL version:
|
||||||
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
|
Redis version:
|
||||||
* Browser:
|
Your OS:
|
||||||
<!-- Example: Chrome 113.0.5672.126 -->
|
Your browser:
|
||||||
* Server URL:
|
|
||||||
<!-- Example: misskey.io -->
|
|
||||||
* Misskey:
|
|
||||||
13.x.x
|
|
||||||
|
|
||||||
### 🛰 Backend (for server admin)
|
|
||||||
<!-- If you are using a managed service, put that after the version. -->
|
|
||||||
|
|
||||||
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
|
|
||||||
* Misskey: 13.x.x
|
|
||||||
* Node: 20.x.x
|
|
||||||
* PostgreSQL: 15.x.x
|
|
||||||
* Redis: 7.x.x
|
|
||||||
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
|
|
||||||
|
2
.github/workflows/storybook.yml
vendored
2
.github/workflows/storybook.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [18.x]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [18.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- uses: actions/checkout@v3.3.0
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [18.x]
|
||||||
browser: [chrome]
|
browser: [chrome]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [18.x]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [18.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- uses: actions/checkout@v3.3.0
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -64,6 +64,3 @@ temp
|
|||||||
*.blend3
|
*.blend3
|
||||||
*.blend4
|
*.blend4
|
||||||
*.blend5
|
*.blend5
|
||||||
|
|
||||||
# VSCode addon
|
|
||||||
.favorites.json
|
|
||||||
|
@@ -1 +1 @@
|
|||||||
20.3.1
|
18.16.0
|
||||||
|
48
CHANGELOG.md
48
CHANGELOG.md
@@ -12,51 +12,6 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 13.x.x (unreleased)
|
|
||||||
|
|
||||||
### General
|
|
||||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
|
||||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
|
||||||
|
|
||||||
### Client
|
|
||||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
|
||||||
- ドライブファイルのメニューで画像をクロップできるように
|
|
||||||
- 画像を動画と同様に簡単に隠せるように
|
|
||||||
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
|
|
||||||
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
|
|
||||||
- Fix: サーバーメトリクスが90度傾いている
|
|
||||||
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
|
||||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
|
||||||
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
|
|
||||||
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
|
|
||||||
|
|
||||||
### Server
|
|
||||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
|
||||||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
|
||||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
|
||||||
- 全体的なDBクエリのパフォーマンスを向上
|
|
||||||
|
|
||||||
## 13.13.2
|
|
||||||
|
|
||||||
### General
|
|
||||||
- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように
|
|
||||||
- ロールが付与されているユーザーリストを非公開にできるように
|
|
||||||
- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました
|
|
||||||
|
|
||||||
### Client
|
|
||||||
- Fix: タブがバックグラウンドでもstreamが切断されないように
|
|
||||||
|
|
||||||
### Server
|
|
||||||
- Fix: キャッシュが溜まり続けないように
|
|
||||||
|
|
||||||
## 13.13.1
|
|
||||||
|
|
||||||
### Client
|
|
||||||
- Fix: タブがアクティブな間はstreamが切断されないように
|
|
||||||
|
|
||||||
### Server
|
|
||||||
- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正
|
|
||||||
|
|
||||||
## 13.13.0
|
## 13.13.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
@@ -133,12 +88,11 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
|||||||
## 13.12.0
|
## 13.12.0
|
||||||
|
|
||||||
### NOTE
|
### NOTE
|
||||||
- Node.js 18.16.0以上が必要になりました
|
- Node.js 18.6.0以上が必要になりました
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||||
- Meilisearchを全文検索に使用できるようになりました
|
- Meilisearchを全文検索に使用できるようになりました
|
||||||
* 「フォロワーのみ」の投稿は検索結果に表示されません。
|
|
||||||
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
||||||
- ユーザーへの自分用メモ機能
|
- ユーザーへの自分用メモ機能
|
||||||
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
||||||
|
@@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue.
|
|||||||

|

|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
During development, it is useful to use the
|
During development, it is useful to use the
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm dev
|
pnpm dev
|
||||||
@@ -150,7 +150,7 @@ Prepare DB/Redis for testing.
|
|||||||
```
|
```
|
||||||
docker compose -f packages/backend/test/docker-compose.yml up
|
docker compose -f packages/backend/test/docker-compose.yml up
|
||||||
```
|
```
|
||||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||||
|
|
||||||
Run all test.
|
Run all test.
|
||||||
```
|
```
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# syntax = docker/dockerfile:1.4
|
# syntax = docker/dockerfile:1.4
|
||||||
|
|
||||||
ARG NODE_VERSION=20.3.1-bullseye
|
ARG NODE_VERSION=18.16.0-bullseye
|
||||||
|
|
||||||
# build assets & compile TypeScript
|
# build assets & compile TypeScript
|
||||||
|
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
<a href="https://misskey-hub.net">
|
<a href="https://misskey-hub.net">
|
||||||
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
|
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a href="https://misskey-hub.net/instances.html">
|
<a href="https://misskey-hub.net/instances.html">
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<a href="https://www.patreon.com/syuilo">
|
<a href="https://www.patreon.com/syuilo">
|
||||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
|
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://codecov.io/gh/misskey-dev/misskey)
|
[](https://codecov.io/gh/misskey-dev/misskey)
|
||||||
|
@@ -23,13 +23,13 @@
|
|||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
<style>
|
<style>
|
||||||
#g8 {
|
#g8 {
|
||||||
animation-name: floating;
|
animation-name: floating;
|
||||||
animation-duration: 3s;
|
animation-duration: 3s;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes floating {
|
@keyframes floating {
|
||||||
0% { transform: translate(0, 0px); }
|
0% { transform: translate(0, 0px); }
|
||||||
50% { transform: translate(0, -5px); }
|
50% { transform: translate(0, -5px); }
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -56,7 +56,7 @@ describe('After setup instance', () => {
|
|||||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||||
cy.get('[data-cy-signup-rules-continue]').click();
|
cy.get('[data-cy-signup-rules-continue]').click();
|
||||||
|
|
||||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||||
cy.get('[data-cy-signup-username] input').type('alice');
|
cy.get('[data-cy-signup-username] input').type('alice');
|
||||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||||
|
@@ -991,7 +991,7 @@ postToTheChannel: "In Kanal senden"
|
|||||||
cannotBeChangedLater: "Kann später nicht mehr geändert werden."
|
cannotBeChangedLater: "Kann später nicht mehr geändert werden."
|
||||||
reactionAcceptance: "Reaktionsannahme"
|
reactionAcceptance: "Reaktionsannahme"
|
||||||
likeOnly: "Nur \"Gefällt mir\""
|
likeOnly: "Nur \"Gefällt mir\""
|
||||||
likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)"
|
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
|
||||||
nonSensitiveOnly: "Keine Sensitiven"
|
nonSensitiveOnly: "Keine Sensitiven"
|
||||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
|
||||||
rolesAssignedToMe: "Mir zugewiesene Rollen"
|
rolesAssignedToMe: "Mir zugewiesene Rollen"
|
||||||
@@ -1062,7 +1062,6 @@ later: "Später"
|
|||||||
goToMisskey: "Zu Misskey"
|
goToMisskey: "Zu Misskey"
|
||||||
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
|
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
|
||||||
installed: "Installiert"
|
installed: "Installiert"
|
||||||
branding: "Branding"
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
||||||
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
||||||
@@ -1094,7 +1093,7 @@ _accountMigration:
|
|||||||
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
|
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden."
|
||||||
movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich."
|
movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich."
|
||||||
postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen."
|
postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen."
|
||||||
movedTo: "Neues Konto:"
|
movedTo: "Umzugsziel:"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Freigeschaltet am"
|
earnedAt: "Freigeschaltet am"
|
||||||
_types:
|
_types:
|
||||||
@@ -1348,7 +1347,7 @@ _role:
|
|||||||
condition: "Bedingung"
|
condition: "Bedingung"
|
||||||
isConditionalRole: "Dies ist eine konditionale Rolle."
|
isConditionalRole: "Dies ist eine konditionale Rolle."
|
||||||
isPublic: "Öffentliche Rolle"
|
isPublic: "Öffentliche Rolle"
|
||||||
descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt."
|
descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt."
|
||||||
options: "Optionen"
|
options: "Optionen"
|
||||||
policies: "Richtlinien"
|
policies: "Richtlinien"
|
||||||
baseRole: "Rollenvorlage"
|
baseRole: "Rollenvorlage"
|
||||||
@@ -1357,8 +1356,8 @@ _role:
|
|||||||
iconUrl: "Icon-URL"
|
iconUrl: "Icon-URL"
|
||||||
asBadge: "Als Abzeichen anzeigen"
|
asBadge: "Als Abzeichen anzeigen"
|
||||||
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
|
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
|
||||||
isExplorable: "Benutzerliste veröffentlichen"
|
isExplorable: "Rollenchronik veröffentlichen"
|
||||||
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich."
|
descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht."
|
||||||
displayOrder: "Position"
|
displayOrder: "Position"
|
||||||
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
|
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
|
||||||
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
|
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
|
||||||
|
@@ -991,7 +991,7 @@ postToTheChannel: "Post to channel"
|
|||||||
cannotBeChangedLater: "This cannot be changed later."
|
cannotBeChangedLater: "This cannot be changed later."
|
||||||
reactionAcceptance: "Reaction Acceptance"
|
reactionAcceptance: "Reaction Acceptance"
|
||||||
likeOnly: "Only likes"
|
likeOnly: "Only likes"
|
||||||
likeOnlyForRemote: "All (Only likes for remote instances)"
|
likeOnlyForRemote: "Only likes for remote instances"
|
||||||
nonSensitiveOnly: "Non-sensitive only"
|
nonSensitiveOnly: "Non-sensitive only"
|
||||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
|
||||||
rolesAssignedToMe: "Roles assigned to me"
|
rolesAssignedToMe: "Roles assigned to me"
|
||||||
@@ -1062,7 +1062,6 @@ later: "Later"
|
|||||||
goToMisskey: "To Misskey"
|
goToMisskey: "To Misskey"
|
||||||
additionalEmojiDictionary: "Additional emoji dictionaries"
|
additionalEmojiDictionary: "Additional emoji dictionaries"
|
||||||
installed: "Installed"
|
installed: "Installed"
|
||||||
branding: "Branding"
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Your account was successfully created!"
|
accountCreated: "Your account was successfully created!"
|
||||||
letsStartAccountSetup: "For starters, let's set up your profile."
|
letsStartAccountSetup: "For starters, let's set up your profile."
|
||||||
@@ -1094,7 +1093,7 @@ _accountMigration:
|
|||||||
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
|
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
|
||||||
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
|
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
|
||||||
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
|
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
|
||||||
movedTo: "New account:"
|
movedTo: "Account to move to:"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Unlocked at"
|
earnedAt: "Unlocked at"
|
||||||
_types:
|
_types:
|
||||||
@@ -1348,7 +1347,7 @@ _role:
|
|||||||
condition: "Condition"
|
condition: "Condition"
|
||||||
isConditionalRole: "This is a conditional role."
|
isConditionalRole: "This is a conditional role."
|
||||||
isPublic: "Public role"
|
isPublic: "Public role"
|
||||||
descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users."
|
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
|
||||||
options: "Options"
|
options: "Options"
|
||||||
policies: "Policies"
|
policies: "Policies"
|
||||||
baseRole: "Role template"
|
baseRole: "Role template"
|
||||||
@@ -1357,8 +1356,8 @@ _role:
|
|||||||
iconUrl: "Icon URL"
|
iconUrl: "Icon URL"
|
||||||
asBadge: "Show as badge"
|
asBadge: "Show as badge"
|
||||||
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
|
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
|
||||||
isExplorable: "Make role explorable"
|
isExplorable: "Role timeline is public"
|
||||||
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
|
descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set."
|
||||||
displayOrder: "Position"
|
displayOrder: "Position"
|
||||||
descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
|
descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
|
||||||
canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
|
canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
|
||||||
|
11
locales/index.d.ts
vendored
11
locales/index.d.ts
vendored
@@ -139,10 +139,8 @@ export interface Locale {
|
|||||||
"suspendConfirm": string;
|
"suspendConfirm": string;
|
||||||
"unsuspendConfirm": string;
|
"unsuspendConfirm": string;
|
||||||
"selectList": string;
|
"selectList": string;
|
||||||
"editList": string;
|
|
||||||
"selectChannel": string;
|
"selectChannel": string;
|
||||||
"selectAntenna": string;
|
"selectAntenna": string;
|
||||||
"editAntenna": string;
|
|
||||||
"selectWidget": string;
|
"selectWidget": string;
|
||||||
"editWidgets": string;
|
"editWidgets": string;
|
||||||
"editWidgetsExit": string;
|
"editWidgetsExit": string;
|
||||||
@@ -316,7 +314,7 @@ export interface Locale {
|
|||||||
"rename": string;
|
"rename": string;
|
||||||
"avatar": string;
|
"avatar": string;
|
||||||
"banner": string;
|
"banner": string;
|
||||||
"displayOfSensitiveMedia": string;
|
"nsfw": string;
|
||||||
"whenServerDisconnected": string;
|
"whenServerDisconnected": string;
|
||||||
"disconnectedFromServer": string;
|
"disconnectedFromServer": string;
|
||||||
"reload": string;
|
"reload": string;
|
||||||
@@ -1067,10 +1065,6 @@ export interface Locale {
|
|||||||
"goToMisskey": string;
|
"goToMisskey": string;
|
||||||
"additionalEmojiDictionary": string;
|
"additionalEmojiDictionary": string;
|
||||||
"installed": string;
|
"installed": string;
|
||||||
"branding": string;
|
|
||||||
"enableServerMachineStats": string;
|
|
||||||
"enableIdenticonGeneration": string;
|
|
||||||
"turnOffToImprovePerformance": string;
|
|
||||||
"_initialAccountSetting": {
|
"_initialAccountSetting": {
|
||||||
"accountCreated": string;
|
"accountCreated": string;
|
||||||
"letsStartAccountSetup": string;
|
"letsStartAccountSetup": string;
|
||||||
@@ -1531,7 +1525,6 @@ export interface Locale {
|
|||||||
"back": string;
|
"back": string;
|
||||||
"reduceFrequencyOfThisAd": string;
|
"reduceFrequencyOfThisAd": string;
|
||||||
"hide": string;
|
"hide": string;
|
||||||
"timezoneinfo": string;
|
|
||||||
};
|
};
|
||||||
"_forgotPassword": {
|
"_forgotPassword": {
|
||||||
"enterEmail": string;
|
"enterEmail": string;
|
||||||
@@ -1593,7 +1586,7 @@ export interface Locale {
|
|||||||
"morePatrons": string;
|
"morePatrons": string;
|
||||||
"patrons": string;
|
"patrons": string;
|
||||||
};
|
};
|
||||||
"_displayOfSensitiveMedia": {
|
"_nsfw": {
|
||||||
"respect": string;
|
"respect": string;
|
||||||
"ignore": string;
|
"ignore": string;
|
||||||
"force": string;
|
"force": string;
|
||||||
|
@@ -112,7 +112,7 @@ pinnedNote: "ピン留めされたノート"
|
|||||||
pinned: "ピン留め"
|
pinned: "ピン留め"
|
||||||
you: "あなた"
|
you: "あなた"
|
||||||
clickToShow: "クリックして表示"
|
clickToShow: "クリックして表示"
|
||||||
sensitive: "センシティブ"
|
sensitive: "閲覧注意"
|
||||||
add: "追加"
|
add: "追加"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
reactions: "リアクション"
|
reactions: "リアクション"
|
||||||
@@ -120,8 +120,8 @@ reactionSetting: "ピッカーに表示するリアクション"
|
|||||||
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
||||||
rememberNoteVisibility: "公開範囲を記憶する"
|
rememberNoteVisibility: "公開範囲を記憶する"
|
||||||
attachCancel: "添付取り消し"
|
attachCancel: "添付取り消し"
|
||||||
markAsSensitive: "センシティブとして設定"
|
markAsSensitive: "閲覧注意にする"
|
||||||
unmarkAsSensitive: "センシティブを解除する"
|
unmarkAsSensitive: "閲覧注意を解除する"
|
||||||
enterFileName: "ファイル名を入力"
|
enterFileName: "ファイル名を入力"
|
||||||
mute: "ミュート"
|
mute: "ミュート"
|
||||||
unmute: "ミュート解除"
|
unmute: "ミュート解除"
|
||||||
@@ -136,10 +136,8 @@ unblockConfirm: "ブロック解除しますか?"
|
|||||||
suspendConfirm: "凍結しますか?"
|
suspendConfirm: "凍結しますか?"
|
||||||
unsuspendConfirm: "解凍しますか?"
|
unsuspendConfirm: "解凍しますか?"
|
||||||
selectList: "リストを選択"
|
selectList: "リストを選択"
|
||||||
editList: "リストを編集"
|
|
||||||
selectChannel: "チャンネルを選択"
|
selectChannel: "チャンネルを選択"
|
||||||
selectAntenna: "アンテナを選択"
|
selectAntenna: "アンテナを選択"
|
||||||
editAntenna: "アンテナを編集"
|
|
||||||
selectWidget: "ウィジェットを選択"
|
selectWidget: "ウィジェットを選択"
|
||||||
editWidgets: "ウィジェットを編集"
|
editWidgets: "ウィジェットを編集"
|
||||||
editWidgetsExit: "編集を終了"
|
editWidgetsExit: "編集を終了"
|
||||||
@@ -313,7 +311,7 @@ copyUrl: "URLをコピー"
|
|||||||
rename: "名前を変更"
|
rename: "名前を変更"
|
||||||
avatar: "アイコン"
|
avatar: "アイコン"
|
||||||
banner: "バナー"
|
banner: "バナー"
|
||||||
displayOfSensitiveMedia: "センシティブなメディアの表示"
|
nsfw: "閲覧注意"
|
||||||
whenServerDisconnected: "サーバーとの接続が失われたとき"
|
whenServerDisconnected: "サーバーとの接続が失われたとき"
|
||||||
disconnectedFromServer: "サーバーから切断されました"
|
disconnectedFromServer: "サーバーから切断されました"
|
||||||
reload: "リロード"
|
reload: "リロード"
|
||||||
@@ -695,7 +693,7 @@ driveUsage: "ドライブ使用量"
|
|||||||
noCrawle: "クローラーによるインデックスを拒否"
|
noCrawle: "クローラーによるインデックスを拒否"
|
||||||
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
|
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
|
||||||
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
|
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
|
||||||
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
|
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
|
||||||
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
|
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
|
||||||
disableShowingAnimatedImages: "アニメーション画像を再生しない"
|
disableShowingAnimatedImages: "アニメーション画像を再生しない"
|
||||||
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
|
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
|
||||||
@@ -922,8 +920,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
|
|||||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||||
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
||||||
beta: "ベータ"
|
beta: "ベータ"
|
||||||
enableAutoSensitive: "自動センシティブ判定"
|
enableAutoSensitive: "自動NSFW判定"
|
||||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
||||||
navbar: "ナビゲーションバー"
|
navbar: "ナビゲーションバー"
|
||||||
shuffle: "シャッフル"
|
shuffle: "シャッフル"
|
||||||
@@ -1064,10 +1062,6 @@ later: "あとで"
|
|||||||
goToMisskey: "Misskeyへ"
|
goToMisskey: "Misskeyへ"
|
||||||
additionalEmojiDictionary: "絵文字の追加辞書"
|
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
installed: "インストール済み"
|
installed: "インストール済み"
|
||||||
branding: "ブランディング"
|
|
||||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
|
||||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
|
||||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
|
||||||
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "アカウントの作成が完了しました!"
|
accountCreated: "アカウントの作成が完了しました!"
|
||||||
@@ -1357,8 +1351,8 @@ _role:
|
|||||||
conditional: "コンディショナル"
|
conditional: "コンディショナル"
|
||||||
condition: "条件"
|
condition: "条件"
|
||||||
isConditionalRole: "これはコンディショナルロールです。"
|
isConditionalRole: "これはコンディショナルロールです。"
|
||||||
isPublic: "公開ロール"
|
isPublic: "ロールを公開"
|
||||||
descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。"
|
descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。"
|
||||||
options: "オプション"
|
options: "オプション"
|
||||||
policies: "ポリシー"
|
policies: "ポリシー"
|
||||||
baseRole: "ベースロール"
|
baseRole: "ベースロール"
|
||||||
@@ -1367,8 +1361,8 @@ _role:
|
|||||||
iconUrl: "アイコン画像のURL"
|
iconUrl: "アイコン画像のURL"
|
||||||
asBadge: "バッジとして表示"
|
asBadge: "バッジとして表示"
|
||||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||||
isExplorable: "ユーザーを見つけやすくする"
|
isExplorable: "ロールタイムラインを公開"
|
||||||
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
|
descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。"
|
||||||
displayOrder: "表示順"
|
displayOrder: "表示順"
|
||||||
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
|
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
|
||||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||||
@@ -1417,7 +1411,7 @@ _sensitiveMediaDetection:
|
|||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
sensitivity: "検出感度"
|
sensitivity: "検出感度"
|
||||||
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
|
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
|
||||||
setSensitiveFlagAutomatically: "センシティブフラグを設定する"
|
setSensitiveFlagAutomatically: "NSFWフラグを設定する"
|
||||||
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
|
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
|
||||||
analyzeVideos: "動画の解析を有効化"
|
analyzeVideos: "動画の解析を有効化"
|
||||||
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
|
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
|
||||||
@@ -1451,7 +1445,6 @@ _ad:
|
|||||||
back: "戻る"
|
back: "戻る"
|
||||||
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
|
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
|
||||||
hide: "表示しない"
|
hide: "表示しない"
|
||||||
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
|
|
||||||
|
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
||||||
@@ -1511,9 +1504,9 @@ _aboutMisskey:
|
|||||||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
||||||
patrons: "支援者"
|
patrons: "支援者"
|
||||||
|
|
||||||
_displayOfSensitiveMedia:
|
_nsfw:
|
||||||
respect: "センシティブ設定されたメディアを隠す"
|
respect: "閲覧注意のメディアは隠す"
|
||||||
ignore: "センシティブ設定されたメディアを隠さない"
|
ignore: "閲覧注意のメディアを隠さない"
|
||||||
force: "常にメディアを隠す"
|
force: "常にメディアを隠す"
|
||||||
|
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
|
@@ -792,7 +792,6 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで"
|
|||||||
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
|
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
|
||||||
configure: "設定する"
|
configure: "設定する"
|
||||||
postToGallery: "ギャラリーへ投稿"
|
postToGallery: "ギャラリーへ投稿"
|
||||||
postToHashtag: "このハッシュタグで投稿"
|
|
||||||
gallery: "ギャラリー"
|
gallery: "ギャラリー"
|
||||||
recentPosts: "最近の投稿"
|
recentPosts: "最近の投稿"
|
||||||
popularPosts: "人気の投稿"
|
popularPosts: "人気の投稿"
|
||||||
@@ -826,7 +825,6 @@ translatedFrom: "{x}から翻訳するで"
|
|||||||
accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
|
accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
|
||||||
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
|
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
|
||||||
aiChanMode: "藍モードやで"
|
aiChanMode: "藍モードやで"
|
||||||
devMode: "開発者モード"
|
|
||||||
keepCw: "CWを維持するで"
|
keepCw: "CWを維持するで"
|
||||||
pubSub: "Pub/Subのアカウント"
|
pubSub: "Pub/Subのアカウント"
|
||||||
lastCommunication: "直近の通信"
|
lastCommunication: "直近の通信"
|
||||||
@@ -836,8 +834,6 @@ breakFollow: "フォロワーを解除するで"
|
|||||||
breakFollowConfirm: "フォロワー解除してもええか?"
|
breakFollowConfirm: "フォロワー解除してもええか?"
|
||||||
itsOn: "オンになっとるよ"
|
itsOn: "オンになっとるよ"
|
||||||
itsOff: "オフになってるで"
|
itsOff: "オフになってるで"
|
||||||
on: "オン"
|
|
||||||
off: "オフ"
|
|
||||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
|
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
|
||||||
unread: "未読"
|
unread: "未読"
|
||||||
filter: "フィルタ"
|
filter: "フィルタ"
|
||||||
@@ -992,8 +988,6 @@ cannotBeChangedLater: "後からは変えられへんで。"
|
|||||||
reactionAcceptance: "ツッコミの受け入れ"
|
reactionAcceptance: "ツッコミの受け入れ"
|
||||||
likeOnly: "いいねだけ"
|
likeOnly: "いいねだけ"
|
||||||
likeOnlyForRemote: "リモートからはいいねだけな"
|
likeOnlyForRemote: "リモートからはいいねだけな"
|
||||||
nonSensitiveOnly: "センシティブじゃないやつだけ"
|
|
||||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)"
|
|
||||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||||
resetPasswordConfirm: "パスワード作り直すんでええな?"
|
resetPasswordConfirm: "パスワード作り直すんでええな?"
|
||||||
sensitiveWords: "けったいな単語"
|
sensitiveWords: "けったいな単語"
|
||||||
@@ -1051,17 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて"
|
|||||||
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
|
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
|
||||||
options: "オプション"
|
options: "オプション"
|
||||||
specifyUser: "ユーザー指定"
|
specifyUser: "ユーザー指定"
|
||||||
failedToPreviewUrl: "プレビューできへん"
|
|
||||||
update: "更新"
|
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
|
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
|
||||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
|
||||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。"
|
|
||||||
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
|
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
|
||||||
changeReactionConfirm: "ツッコミを別のに変えるか?"
|
changeReactionConfirm: "ツッコミを別のに変えるか?"
|
||||||
later: "あとで"
|
|
||||||
goToMisskey: "Misskeyへ"
|
|
||||||
additionalEmojiDictionary: "絵文字の追加辞書"
|
|
||||||
installed: "インストール済み"
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "アカウント作り終わったで。"
|
accountCreated: "アカウント作り終わったで。"
|
||||||
letsStartAccountSetup: "アカウントの初期設定をしよか。"
|
letsStartAccountSetup: "アカウントの初期設定をしよか。"
|
||||||
@@ -1076,7 +1063,6 @@ _initialAccountSetting:
|
|||||||
haveFun: "{name}、楽しんでな~"
|
haveFun: "{name}、楽しんでな~"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
||||||
skipAreYouSure: "初期設定飛ばすか?"
|
skipAreYouSure: "初期設定飛ばすか?"
|
||||||
laterAreYouSure: "初期設定あとでやり直すん?"
|
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
_lang_: "Türkçe"
|
_lang_: "Türkçe"
|
||||||
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
||||||
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>Misskey</b>Dünya'nın en sunucularında biri。"
|
|
||||||
monthAndDay: "{month}Ay {day}Gün"
|
monthAndDay: "{month}Ay {day}Gün"
|
||||||
search: "Arama"
|
search: "Arama"
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirim"
|
||||||
@@ -14,9 +13,7 @@ cancel: "İptal"
|
|||||||
enterUsername: "Kullanıcı adınızı giriniz"
|
enterUsername: "Kullanıcı adınızı giriniz"
|
||||||
noNotes: "Notlar mevcut değil."
|
noNotes: "Notlar mevcut değil."
|
||||||
noNotifications: "Bildirim bulunmuyor"
|
noNotifications: "Bildirim bulunmuyor"
|
||||||
instance: "Sunucu"
|
|
||||||
settings: "Ayarlar"
|
settings: "Ayarlar"
|
||||||
notificationSettings: "Bildirim Ayarları"
|
|
||||||
basicSettings: "Temel Ayarlar"
|
basicSettings: "Temel Ayarlar"
|
||||||
otherSettings: "Diğer Ayarlar"
|
otherSettings: "Diğer Ayarlar"
|
||||||
openInWindow: "Bir pencere ile aç"
|
openInWindow: "Bir pencere ile aç"
|
||||||
@@ -24,11 +21,9 @@ profile: "Profil"
|
|||||||
timeline: "Zaman çizelgesi"
|
timeline: "Zaman çizelgesi"
|
||||||
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı"
|
noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı"
|
||||||
login: "Giriş Yap "
|
login: "Giriş Yap "
|
||||||
loggingIn: "Oturum aç"
|
|
||||||
logout: "Çıkış Yap"
|
logout: "Çıkış Yap"
|
||||||
signup: "Kayıt Ol"
|
signup: "Kayıt Ol"
|
||||||
uploading: "Yükleniyor"
|
uploading: "Yükleniyor"
|
||||||
save: "Kaydet"
|
|
||||||
users: "Kullanıcı"
|
users: "Kullanıcı"
|
||||||
addUser: "Kullanıcı Ekle"
|
addUser: "Kullanıcı Ekle"
|
||||||
favorite: "Favoriler"
|
favorite: "Favoriler"
|
||||||
@@ -36,7 +31,6 @@ favorites: "Favoriler"
|
|||||||
unfavorite: "Favorilerden Kaldır"
|
unfavorite: "Favorilerden Kaldır"
|
||||||
favorited: "Favorilerime eklendi."
|
favorited: "Favorilerime eklendi."
|
||||||
alreadyFavorited: "Zaten favorilerinizde kayıtlı."
|
alreadyFavorited: "Zaten favorilerinizde kayıtlı."
|
||||||
cantFavorite: "Favorilere kayıt yapılamadı"
|
|
||||||
pin: "Sabitlenmiş"
|
pin: "Sabitlenmiş"
|
||||||
unpin: "Sabitlemeyi kaldır"
|
unpin: "Sabitlemeyi kaldır"
|
||||||
copyContent: "İçeriği kopyala"
|
copyContent: "İçeriği kopyala"
|
||||||
@@ -46,88 +40,23 @@ deleteAndEdit: "Sil ve yeniden düzenle"
|
|||||||
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
|
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
|
||||||
addToList: "Listeye ekle"
|
addToList: "Listeye ekle"
|
||||||
sendMessage: "Mesaj Gönder"
|
sendMessage: "Mesaj Gönder"
|
||||||
copyRSS: "RSSKopyala"
|
|
||||||
copyUsername: "Kullanıcı Adını Kopyala"
|
copyUsername: "Kullanıcı Adını Kopyala"
|
||||||
copyUserId: "KullanıcıyıKopyala"
|
|
||||||
copyNoteId: "Kimlik notunu kopyala"
|
|
||||||
searchUser: "Kullanıcıları ara"
|
searchUser: "Kullanıcıları ara"
|
||||||
reply: "yanıt"
|
|
||||||
loadMore: "Devamını yükle"
|
|
||||||
showMore: "Devamını yükle"
|
|
||||||
lists: "Listeler"
|
|
||||||
noLists: "Liste yok"
|
|
||||||
note: "not"
|
|
||||||
notes: "notlar"
|
|
||||||
following: "takipçi"
|
|
||||||
followers: "takipçi"
|
|
||||||
followsYou: "seni takip ediyor"
|
|
||||||
createList: "Liste oluştur"
|
|
||||||
manageLists: "Yönetici Listeleri"
|
|
||||||
error: "hata"
|
|
||||||
follow: "takipçi"
|
|
||||||
followRequest: "Takip isteği"
|
|
||||||
followRequests: "Takip istekleri"
|
|
||||||
unfollow: "takip etmeyi bırak"
|
|
||||||
followRequestPending: "Bekleyen Takip Etme Talebi"
|
|
||||||
enterEmoji: "Emoji Giriniz"
|
|
||||||
renote: "vazgeçme"
|
|
||||||
unrenote: "not alma"
|
|
||||||
renoted: "yeniden adlandırılmış"
|
|
||||||
cantRenote: "Ayrılamama"
|
|
||||||
cantReRenote: "not alabilirmiyim"
|
|
||||||
quote: "alıntı"
|
|
||||||
pinnedNote: "Sabitlenen"
|
|
||||||
pinned: "Sabitlenmiş"
|
pinned: "Sabitlenmiş"
|
||||||
you: "sen"
|
|
||||||
unmute: "sesi aç"
|
|
||||||
renoteMute: "sesi kapat"
|
|
||||||
renoteUnmute: "sesi açmayı iptal et"
|
|
||||||
block: "engelle"
|
|
||||||
unblock: "engellemeyi kaldır"
|
|
||||||
suspend: "askıya al"
|
|
||||||
unsuspend: "askıya alma"
|
|
||||||
blockConfirm: "Onayı engelle"
|
|
||||||
unblockConfirm: "engellemeyi kaldır onayla"
|
|
||||||
selectChannel: "Kanal seç"
|
|
||||||
flagAsBot: "Bot olarak işaretle"
|
|
||||||
instances: "Sunucu"
|
|
||||||
remove: "Sil"
|
remove: "Sil"
|
||||||
pinnedNotes: "Sabitlenen"
|
|
||||||
userList: "Listeler"
|
|
||||||
smtpUser: "Kullanıcı Adı"
|
smtpUser: "Kullanıcı Adı"
|
||||||
smtpPass: "Şifre"
|
smtpPass: "Şifre"
|
||||||
user: "Kullanıcı"
|
user: "Kullanıcı"
|
||||||
searchByGoogle: "Arama"
|
searchByGoogle: "Arama"
|
||||||
_theme:
|
|
||||||
keys:
|
|
||||||
renote: "vazgeçme"
|
|
||||||
_sfx:
|
_sfx:
|
||||||
note: "notlar"
|
|
||||||
notification: "Bildirim"
|
notification: "Bildirim"
|
||||||
_widgets:
|
_widgets:
|
||||||
profile: "Profil"
|
profile: "Profil"
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirim"
|
||||||
timeline: "Zaman çizelgesi"
|
timeline: "Zaman çizelgesi"
|
||||||
_cw:
|
|
||||||
show: "Devamını yükle"
|
|
||||||
_visibility:
|
|
||||||
followers: "takipçi"
|
|
||||||
_profile:
|
_profile:
|
||||||
username: "Kullanıcı Adı"
|
username: "Kullanıcı Adı"
|
||||||
_exportOrImport:
|
|
||||||
followingList: "takipçi"
|
|
||||||
blockingList: "engelle"
|
|
||||||
userLists: "Listeler"
|
|
||||||
_notification:
|
|
||||||
_types:
|
|
||||||
follow: "takipçi"
|
|
||||||
renote: "vazgeçme"
|
|
||||||
quote: "alıntı"
|
|
||||||
_actions:
|
|
||||||
reply: "yanıt"
|
|
||||||
renote: "vazgeçme"
|
|
||||||
_deck:
|
_deck:
|
||||||
_columns:
|
_columns:
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirim"
|
||||||
tl: "Zaman çizelgesi"
|
tl: "Zaman çizelgesi"
|
||||||
list: "Listeler"
|
|
||||||
|
@@ -1060,7 +1060,6 @@ cancelReactionConfirm: "要取消回应吗?"
|
|||||||
changeReactionConfirm: "要更改回应吗?"
|
changeReactionConfirm: "要更改回应吗?"
|
||||||
later: "一会再说"
|
later: "一会再说"
|
||||||
goToMisskey: "去往Misskey"
|
goToMisskey: "去往Misskey"
|
||||||
additionalEmojiDictionary: "表情符号追加字典"
|
|
||||||
installed: "已安装"
|
installed: "已安装"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "账户创建完成了!"
|
accountCreated: "账户创建完成了!"
|
||||||
|
@@ -1062,7 +1062,6 @@ later: "稍後再說"
|
|||||||
goToMisskey: "往Misskey"
|
goToMisskey: "往Misskey"
|
||||||
additionalEmojiDictionary: "表情符號的附加辭典"
|
additionalEmojiDictionary: "表情符號的附加辭典"
|
||||||
installed: "已安裝"
|
installed: "已安裝"
|
||||||
branding: "品牌宣傳"
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "帳戶已建立完成!"
|
accountCreated: "帳戶已建立完成!"
|
||||||
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
|
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
|
||||||
|
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.14.0-beta.1",
|
"version": "13.13.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"migrateandstart": "pnpm migrate && pnpm start",
|
"migrateandstart": "pnpm migrate && pnpm start",
|
||||||
"gulp": "pnpm exec gulp build",
|
"gulp": "pnpm exec gulp build",
|
||||||
"watch": "pnpm dev",
|
"watch": "pnpm dev",
|
||||||
"dev": "node ./scripts/dev.mjs",
|
"dev": "node ./scripts/dev.js",
|
||||||
"lint": "pnpm -r lint",
|
"lint": "pnpm -r lint",
|
||||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||||
"cy:run": "pnpm cypress run",
|
"cy:run": "pnpm cypress run",
|
||||||
@@ -44,23 +44,23 @@
|
|||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"execa": "7.1.1",
|
"execa": "5.1.1",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-cssnano": "2.1.3",
|
"gulp-cssnano": "2.1.3",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
"gulp-replace": "1.1.4",
|
"gulp-replace": "1.1.4",
|
||||||
"gulp-terser": "2.1.0",
|
"gulp-terser": "2.1.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.59.8",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.17.0",
|
"cypress": "12.13.0",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.41.0",
|
||||||
"start-server-and-test": "2.0.0"
|
"start-server-and-test": "2.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["*"]
|
"@/*": ["*"]
|
||||||
},
|
},
|
||||||
"target": "es2022"
|
"target": "es2021"
|
||||||
},
|
},
|
||||||
"minify": false
|
"minify": false
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@@ -1,9 +0,0 @@
|
|||||||
export class ad1677054292210 {
|
|
||||||
name = 'ad1677054292210';
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
|
|
||||||
}
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
export class ErrorImageUrl1685973839966 {
|
|
||||||
name = 'ErrorImageUrl1685973839966'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
export class AddMetaOptions1688280713783 {
|
|
||||||
name = 'AddMetaOptions1688280713783'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,9 +3,6 @@
|
|||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
|
||||||
"node": ">=18.16.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./built/index.js",
|
"start": "node ./built/index.js",
|
||||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||||
@@ -54,36 +51,35 @@
|
|||||||
"utf-8-validate": "^6.0.3"
|
"utf-8-validate": "^6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.367.0",
|
"@aws-sdk/client-s3": "3.321.1",
|
||||||
"@aws-sdk/lib-storage": "3.367.0",
|
"@aws-sdk/lib-storage": "3.321.1",
|
||||||
"@aws-sdk/node-http-handler": "3.360.0",
|
"@aws-sdk/node-http-handler": "3.321.1",
|
||||||
"@bull-board/api": "5.6.0",
|
"@bull-board/api": "5.2.0",
|
||||||
"@bull-board/fastify": "5.6.0",
|
"@bull-board/fastify": "5.2.0",
|
||||||
"@bull-board/ui": "5.6.0",
|
"@bull-board/ui": "5.2.0",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.3.0",
|
"@fastify/cors": "8.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.1.0",
|
||||||
"@fastify/multipart": "7.7.0",
|
"@fastify/multipart": "7.6.0",
|
||||||
"@fastify/static": "6.10.2",
|
"@fastify/static": "6.10.2",
|
||||||
"@fastify/view": "8.0.0",
|
"@fastify/view": "7.4.1",
|
||||||
"@nestjs/common": "10.0.5",
|
"@nestjs/common": "9.4.2",
|
||||||
"@nestjs/core": "10.0.5",
|
"@nestjs/core": "9.4.2",
|
||||||
"@nestjs/testing": "10.0.5",
|
"@nestjs/testing": "9.4.2",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.3.0",
|
"@sinonjs/fake-timers": "10.2.0",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.68",
|
"@swc/core": "1.3.61",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"async-mutex": "^0.4.0",
|
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bullmq": "4.2.0",
|
"bullmq": "3.15.0",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "6.1.0",
|
||||||
"cbor": "9.0.0",
|
"cbor": "9.0.0",
|
||||||
"chalk": "5.2.0",
|
"chalk": "5.2.0",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "0.4.0",
|
||||||
@@ -94,24 +90,23 @@
|
|||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"fastify": "4.19.2",
|
"fastify": "4.17.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "18.5.0",
|
"file-type": "18.4.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "13.0.0",
|
"got": "12.6.0",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "9.20.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"ip-cidr": "3.1.0",
|
"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",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "22.1.0",
|
"jsdom": "22.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.2.0",
|
"jsonld": "8.2.0",
|
||||||
"jsrsasign": "10.8.6",
|
"jsrsasign": "10.8.6",
|
||||||
"meilisearch": "0.33.0",
|
"meilisearch": "0.32.5",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
@@ -122,9 +117,10 @@
|
|||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.1.3",
|
"otpauth": "9.1.2",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.11.1",
|
"pg": "8.11.0",
|
||||||
|
"private-ip": "3.0.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
@@ -133,39 +129,41 @@
|
|||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.19.1",
|
"re2": "1.19.0",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
"rndstr": "1.0.0",
|
||||||
"rss-parser": "3.13.0",
|
"rss-parser": "3.13.0",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.10.0",
|
||||||
"semver": "7.5.3",
|
"seedrandom": "3.0.5",
|
||||||
|
"semver": "7.5.1",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"slacc": "0.0.9",
|
"slacc": "0.0.9",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.18.6",
|
"systeminformation": "5.17.16",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.7",
|
"tsc-alias": "1.8.6",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.17",
|
"typeorm": "0.3.16",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.14",
|
"unzipper": "0.10.14",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.3",
|
"web-push": "3.6.1",
|
||||||
"ws": "8.13.0",
|
"ws": "8.13.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.6.1",
|
"@jest/globals": "29.5.0",
|
||||||
"@swc/jest": "0.2.26",
|
"@swc/jest": "0.2.26",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.2",
|
"@types/archiver": "5.3.2",
|
||||||
@@ -178,18 +176,17 @@
|
|||||||
"@types/jest": "29.5.2",
|
"@types/jest": "29.5.2",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.1",
|
"@types/jsdom": "21.1.1",
|
||||||
"@types/jsonld": "1.5.9",
|
"@types/jsonld": "1.5.8",
|
||||||
"@types/jsrsasign": "10.5.8",
|
"@types/jsrsasign": "10.5.8",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/ms": "^0.7.31",
|
"@types/node": "20.2.5",
|
||||||
"@types/node": "20.4.0",
|
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.8",
|
"@types/nodemailer": "6.4.8",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
"@types/pg": "8.10.2",
|
"@types/pg": "8.10.1",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.6",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/qrcode": "1.5.1",
|
"@types/qrcode": "1.5.0",
|
||||||
"@types/random-seed": "0.3.3",
|
"@types/random-seed": "0.3.3",
|
||||||
"@types/ratelimiter": "3.4.4",
|
"@types/ratelimiter": "3.4.4",
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
@@ -201,19 +198,19 @@
|
|||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/tmp": "0.2.3",
|
"@types/tmp": "0.2.3",
|
||||||
"@types/unzipper": "0.10.6",
|
"@types/unzipper": "0.10.6",
|
||||||
"@types/uuid": "9.0.2",
|
"@types/uuid": "9.0.1",
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.0",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.5",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.59.8",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "2.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.41.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "7.1.1",
|
"execa": "6.1.0",
|
||||||
"jest": "29.6.1",
|
"jest": "29.5.0",
|
||||||
"jest-mock": "29.6.1"
|
"jest-mock": "29.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -96,6 +96,12 @@ function showNodejsVersion(): void {
|
|||||||
const nodejsLogger = bootLogger.createSubLogger('nodejs');
|
const nodejsLogger = bootLogger.createSubLogger('nodejs');
|
||||||
|
|
||||||
nodejsLogger.info(`Version ${process.version} detected.`);
|
nodejsLogger.info(`Version ${process.version} detected.`);
|
||||||
|
|
||||||
|
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
|
||||||
|
if (semver.lt(process.version, minVersion)) {
|
||||||
|
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfigBoot(): Config {
|
function loadConfigBoot(): Config {
|
||||||
|
@@ -295,7 +295,7 @@ export class AccountMoveService {
|
|||||||
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
|
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
|
||||||
*
|
*
|
||||||
* @param dst movedToUrlを指定するユーザー
|
* @param dst movedToUrlを指定するユーザー
|
||||||
* @param check
|
* @param check
|
||||||
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
|
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
|
||||||
* @returns Promise<LocalUser | RemoteUser | null>
|
* @returns Promise<LocalUser | RemoteUser | null>
|
||||||
*/
|
*/
|
||||||
|
@@ -4,7 +4,6 @@ import { dirname } from 'node:path';
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as nsfw from 'nsfwjs';
|
import * as nsfw from 'nsfwjs';
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { Mutex } from 'async-mutex';
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -18,7 +17,6 @@ let isSupportedCpu: undefined | boolean = undefined;
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiService {
|
export class AiService {
|
||||||
private model: nsfw.NSFWJS;
|
private model: nsfw.NSFWJS;
|
||||||
private modelLoadMutex: Mutex = new Mutex();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@@ -33,22 +31,16 @@ export class AiService {
|
|||||||
const cpuFlags = await this.getCpuFlags();
|
const cpuFlags = await this.getCpuFlags();
|
||||||
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
|
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSupportedCpu) {
|
if (!isSupportedCpu) {
|
||||||
console.error('This CPU cannot use TensorFlow.');
|
console.error('This CPU cannot use TensorFlow.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tf = await import('@tensorflow/tfjs-node');
|
const tf = await import('@tensorflow/tfjs-node');
|
||||||
|
|
||||||
if (this.model == null) {
|
if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
||||||
await this.modelLoadMutex.runExclusive(async () => {
|
|
||||||
if (this.model == null) {
|
|
||||||
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await fs.promises.readFile(path);
|
const buffer = await fs.promises.readFile(path);
|
||||||
const image = await tf.node.decodeImage(buffer, 3) as any;
|
const image = await tf.node.decodeImage(buffer, 3) as any;
|
||||||
try {
|
try {
|
||||||
|
@@ -99,7 +99,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
'MAXLEN', '~', '200',
|
'MAXLEN', '~', '200',
|
||||||
'*',
|
'*',
|
||||||
'note', note.id);
|
'note', note.id);
|
||||||
|
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,16 +112,16 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
|
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
|
||||||
if (note.visibility === 'specified') return false;
|
if (note.visibility === 'specified') return false;
|
||||||
if (note.visibility === 'followers') return false;
|
if (note.visibility === 'followers') return false;
|
||||||
|
|
||||||
if (!antenna.withReplies && note.replyId != null) return false;
|
if (!antenna.withReplies && note.replyId != null) return false;
|
||||||
|
|
||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
const listUsers = (await this.userListJoiningsRepository.findBy({
|
||||||
userListId: antenna.userListId!,
|
userListId: antenna.userListId!,
|
||||||
})).map(x => x.userId);
|
})).map(x => x.userId);
|
||||||
|
|
||||||
if (!listUsers.includes(note.userId)) return false;
|
if (!listUsers.includes(note.userId)) return false;
|
||||||
} else if (antenna.src === 'users') {
|
} else if (antenna.src === 'users') {
|
||||||
const accts = antenna.users.map(x => {
|
const accts = antenna.users.map(x => {
|
||||||
@@ -130,32 +130,32 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keywords = antenna.keywords
|
const keywords = antenna.keywords
|
||||||
// Clean up
|
// Clean up
|
||||||
.map(xs => xs.filter(x => x !== ''))
|
.map(xs => xs.filter(x => x !== ''))
|
||||||
.filter(xs => xs.length > 0);
|
.filter(xs => xs.length > 0);
|
||||||
|
|
||||||
if (keywords.length > 0) {
|
if (keywords.length > 0) {
|
||||||
if (note.text == null && note.cw == null) return false;
|
if (note.text == null && note.cw == null) return false;
|
||||||
|
|
||||||
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
|
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
|
||||||
|
|
||||||
const matched = keywords.some(and =>
|
const matched = keywords.some(and =>
|
||||||
and.every(keyword =>
|
and.every(keyword =>
|
||||||
antenna.caseSensitive
|
antenna.caseSensitive
|
||||||
? _text.includes(keyword)
|
? _text.includes(keyword)
|
||||||
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!matched) return false;
|
if (!matched) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const excludeKeywords = antenna.excludeKeywords
|
const excludeKeywords = antenna.excludeKeywords
|
||||||
// Clean up
|
// Clean up
|
||||||
.map(xs => xs.filter(x => x !== ''))
|
.map(xs => xs.filter(x => x !== ''))
|
||||||
.filter(xs => xs.length > 0);
|
.filter(xs => xs.length > 0);
|
||||||
|
|
||||||
if (excludeKeywords.length > 0) {
|
if (excludeKeywords.length > 0) {
|
||||||
if (note.text == null && note.cw == null) return false;
|
if (note.text == null && note.cw == null) return false;
|
||||||
|
|
||||||
@@ -167,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
? _text.includes(keyword)
|
? _text.includes(keyword)
|
||||||
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
));
|
));
|
||||||
|
|
||||||
if (matched) return false;
|
if (matched) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (antenna.withFile) {
|
if (antenna.withFile) {
|
||||||
if (note.fileIds && note.fileIds.length === 0) return false;
|
if (note.fileIds && note.fileIds.length === 0) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: eval expression
|
// TODO: eval expression
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
this.antennasFetched = true;
|
this.antennasFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.antennas;
|
return this.antennas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,6 +32,11 @@ export class AppLockService {
|
|||||||
return this.lock(`ap-object:${uri}`, timeout);
|
return this.lock(`ap-object:${uri}`, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> {
|
||||||
|
return this.lock(`instance:${host}`, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
|
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
|
||||||
return this.lock(`chart-insert:${lockKey}`, timeout);
|
return this.lock(`chart-insert:${lockKey}`, timeout);
|
||||||
|
@@ -168,17 +168,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
this.userByIdCache.dispose();
|
|
||||||
this.localUserByNativeTokenCache.dispose();
|
|
||||||
this.localUserByIdCache.dispose();
|
|
||||||
this.uriPersonCache.dispose();
|
|
||||||
this.userProfileCache.dispose();
|
|
||||||
this.userMutingsCache.dispose();
|
|
||||||
this.userBlockingCache.dispose();
|
|
||||||
this.userBlockedCache.dispose();
|
|
||||||
this.renoteMutingsCache.dispose();
|
|
||||||
this.userFollowingsCache.dispose();
|
|
||||||
this.userFollowingChannelsCache.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -20,7 +20,7 @@ export class CaptchaService {
|
|||||||
secret,
|
secret,
|
||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await this.httpRequestService.send(url, {
|
const res = await this.httpRequestService.send(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: params.toString(),
|
body: params.toString(),
|
||||||
@@ -28,14 +28,14 @@ export class CaptchaService {
|
|||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
}, { throwErrorWhenResponseNotOk: false });
|
}, { throwErrorWhenResponseNotOk: false });
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`${res.status}`);
|
throw new Error(`${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await res.json() as CaptchaResponse;
|
return await res.json() as CaptchaResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
@@ -73,7 +73,7 @@ export class CaptchaService {
|
|||||||
if (response == null) {
|
if (response == null) {
|
||||||
throw new Error('turnstile-failed: no response provided');
|
throw new Error('turnstile-failed: no response provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||||
throw new Error(`turnstile-request-failed: ${err}`);
|
throw new Error(`turnstile-request-failed: ${err}`);
|
||||||
});
|
});
|
||||||
|
@@ -25,27 +25,27 @@ export class CreateSystemUserService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async createSystemUser(username: string): Promise<User> {
|
public async createSystemUser(username: string): Promise<User> {
|
||||||
const password = uuid();
|
const password = uuid();
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = await bcrypt.genSalt(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
const hash = await bcrypt.hash(password, salt);
|
const hash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateNativeUserToken();
|
const secret = generateNativeUserToken();
|
||||||
|
|
||||||
const keyPair = await genRsaKeyPair(4096);
|
const keyPair = await genRsaKeyPair(4096);
|
||||||
|
|
||||||
let account!: User;
|
let account!: User;
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
const exist = await transactionalEntityManager.findOneBy(User, {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) throw new Error('the user is already exists');
|
if (exist) throw new Error('the user is already exists');
|
||||||
|
|
||||||
account = await transactionalEntityManager.insert(User, {
|
account = await transactionalEntityManager.insert(User, {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -58,25 +58,25 @@ export class CreateSystemUserService {
|
|||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
isBot: true,
|
isBot: true,
|
||||||
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
|
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
|
||||||
|
|
||||||
await transactionalEntityManager.insert(UserKeypair, {
|
await transactionalEntityManager.insert(UserKeypair, {
|
||||||
publicKey: keyPair.publicKey,
|
publicKey: keyPair.publicKey,
|
||||||
privateKey: keyPair.privateKey,
|
privateKey: keyPair.privateKey,
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await transactionalEntityManager.insert(UserProfile, {
|
await transactionalEntityManager.insert(UserProfile, {
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
autoAcceptFollowed: false,
|
autoAcceptFollowed: false,
|
||||||
password: hash,
|
password: hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
await transactionalEntityManager.insert(UsedUsername, {
|
await transactionalEntityManager.insert(UsedUsername, {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
username: username.toLowerCase(),
|
username: username.toLowerCase(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js';
|
|||||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService implements OnApplicationShutdown {
|
export class CustomEmojiService {
|
||||||
private cache: MemoryKVCache<Emoji | null>;
|
private cache: MemoryKVCache<Emoji | null>;
|
||||||
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: updated,
|
emoji: updated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
});
|
});
|
||||||
@@ -215,7 +215,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
|
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
@@ -349,14 +349,4 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.cache.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -28,11 +28,11 @@ export class DeleteAccountService {
|
|||||||
|
|
||||||
// 物理削除する前にDelete activityを送信する
|
// 物理削除する前にDelete activityを送信する
|
||||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||||
|
|
||||||
this.queueService.createDeleteAccountJob(user, {
|
this.queueService.createDeleteAccountJob(user, {
|
||||||
soft: false,
|
soft: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,8 @@ import * as fs from 'node:fs';
|
|||||||
import * as stream from 'node:stream';
|
import * as stream from 'node:stream';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ipaddr from 'ipaddr.js';
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import PrivateIp from 'private-ip';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import { parse } from 'content-disposition';
|
import { parse } from 'content-disposition';
|
||||||
@@ -122,15 +123,15 @@ export class DownloadService {
|
|||||||
public async downloadTextFile(url: string): Promise<string> {
|
public async downloadTextFile(url: string): Promise<string> {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`text file: Temp file is ${path}`);
|
this.logger.info(`text file: Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// write content at URL to temp file
|
// write content at URL to temp file
|
||||||
await this.downloadUrl(url, path);
|
await this.downloadUrl(url, path);
|
||||||
|
|
||||||
const text = await util.promisify(fs.readFile)(path, 'utf8');
|
const text = await util.promisify(fs.readFile)(path, 'utf8');
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -139,14 +140,13 @@ export class DownloadService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isPrivateIp(ip: string): boolean {
|
private isPrivateIp(ip: string): boolean {
|
||||||
const parsedIp = ipaddr.parse(ip);
|
|
||||||
|
|
||||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||||
if (parsedIp.match(ipaddr.parseCIDR(net))) {
|
const cidr = new IPCIDR(net);
|
||||||
|
if (cidr.contains(ip)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedIp.range() !== 'unicast';
|
return PrivateIp(ip) ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,12 +29,12 @@ export class EmailService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
||||||
const meta = await this.metaService.fetch(true);
|
const meta = await this.metaService.fetch(true);
|
||||||
|
|
||||||
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
||||||
const emailSettingUrl = `${this.config.url}/settings/email`;
|
const emailSettingUrl = `${this.config.url}/settings/email`;
|
||||||
|
|
||||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: meta.smtpHost,
|
host: meta.smtpHost,
|
||||||
port: meta.smtpPort,
|
port: meta.smtpPort,
|
||||||
@@ -46,7 +46,7 @@ export class EmailService {
|
|||||||
pass: meta.smtpPass,
|
pass: meta.smtpPass,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: htmlサニタイズ
|
// TODO: htmlサニタイズ
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
@@ -135,7 +135,7 @@ export class EmailService {
|
|||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`Message sent: ${info.messageId}`);
|
this.logger.info(`Message sent: ${info.messageId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err as Error);
|
this.logger.error(err as Error);
|
||||||
@@ -149,12 +149,12 @@ export class EmailService {
|
|||||||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
|
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
|
||||||
}> {
|
}> {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
const exist = await this.userProfilesRepository.countBy({
|
const exist = await this.userProfilesRepository.countBy({
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validated = meta.enableActiveEmailValidation ? await validateEmail({
|
const validated = meta.enableActiveEmailValidation ? await validateEmail({
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
validateRegex: true,
|
validateRegex: true,
|
||||||
@@ -163,9 +163,9 @@ export class EmailService {
|
|||||||
validateDisposable: true, // 捨てアドかどうかチェック
|
validateDisposable: true, // 捨てアドかどうかチェック
|
||||||
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
||||||
}) : { valid: true, reason: null };
|
}) : { valid: true, reason: null };
|
||||||
|
|
||||||
const available = exist === 0 && validated.valid;
|
const available = exist === 0 && validated.valid;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
available,
|
available,
|
||||||
reason: available ? null :
|
reason: available ? null :
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { InstancesRepository } from '@/models/index.js';
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
@@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FederatedInstanceService implements OnApplicationShutdown {
|
export class FederatedInstanceService {
|
||||||
public federatedInstanceCache: RedisKVCache<Instance | null>;
|
public federatedInstanceCache: RedisKVCache<Instance | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -43,19 +43,19 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(host: string): Promise<Instance> {
|
public async fetch(host: string): Promise<Instance> {
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
const cached = await this.federatedInstanceCache.get(host);
|
const cached = await this.federatedInstanceCache.get(host);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const index = await this.instancesRepository.findOneBy({ host });
|
const index = await this.instancesRepository.findOneBy({ host });
|
||||||
|
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
const i = await this.instancesRepository.insert({
|
const i = await this.instancesRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
host,
|
host,
|
||||||
firstRetrievedAt: new Date(),
|
firstRetrievedAt: new Date(),
|
||||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
this.federatedInstanceCache.set(host, i);
|
this.federatedInstanceCache.set(host, i);
|
||||||
return i;
|
return i;
|
||||||
} else {
|
} else {
|
||||||
@@ -74,17 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.raw[0];
|
return response.raw[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.federatedInstanceCache.set(result.host, result);
|
this.federatedInstanceCache.set(result.host, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.federatedInstanceCache.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
@@ -10,7 +12,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import type { DOMWindow } from 'jsdom';
|
import type { DOMWindow } from 'jsdom';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
|
|
||||||
type NodeInfo = {
|
type NodeInfo = {
|
||||||
openRegistrations?: unknown;
|
openRegistrations?: unknown;
|
||||||
@@ -36,49 +37,39 @@ export class FetchInstanceMetadataService {
|
|||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.instancesRepository)
|
||||||
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
private appLockService: AppLockService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async tryLock(host: string): Promise<boolean> {
|
|
||||||
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
|
|
||||||
return mutex !== '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public unlock(host: string): Promise<'OK'> {
|
|
||||||
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
|
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
|
||||||
const host = instance.host;
|
const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host);
|
||||||
// Acquire mutex to ensure no parallel runs
|
|
||||||
if (!await this.tryLock(host)) return;
|
|
||||||
try {
|
|
||||||
if (!force) {
|
|
||||||
const _instance = await this.federatedInstanceService.fetch(host);
|
|
||||||
const now = Date.now();
|
|
||||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
|
||||||
// unlock at the finally caluse
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info(`Fetching metadata of ${instance.host} ...`);
|
if (!force) {
|
||||||
|
const _instance = await this.instancesRepository.findOneBy({ host: instance.host });
|
||||||
|
const now = Date.now();
|
||||||
|
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
||||||
|
unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Fetching metadata of ${instance.host} ...`);
|
||||||
|
|
||||||
|
try {
|
||||||
const [info, dom, manifest] = await Promise.all([
|
const [info, dom, manifest] = await Promise.all([
|
||||||
this.fetchNodeinfo(instance).catch(() => null),
|
this.fetchNodeinfo(instance).catch(() => null),
|
||||||
this.fetchDom(instance).catch(() => null),
|
this.fetchDom(instance).catch(() => null),
|
||||||
this.fetchManifest(instance).catch(() => null),
|
this.fetchManifest(instance).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [favicon, icon, themeColor, name, description] = await Promise.all([
|
const [favicon, icon, themeColor, name, description] = await Promise.all([
|
||||||
this.fetchFaviconUrl(instance, dom).catch(() => null),
|
this.fetchFaviconUrl(instance, dom).catch(() => null),
|
||||||
this.fetchIconUrl(instance, dom, manifest).catch(() => null),
|
this.fetchIconUrl(instance, dom, manifest).catch(() => null),
|
||||||
@@ -86,13 +77,13 @@ export class FetchInstanceMetadataService {
|
|||||||
this.getSiteName(info, dom, manifest).catch(() => null),
|
this.getSiteName(info, dom, manifest).catch(() => null),
|
||||||
this.getDescription(info, dom, manifest).catch(() => null),
|
this.getDescription(info, dom, manifest).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
|
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
infoUpdatedAt: new Date(),
|
infoUpdatedAt: new Date(),
|
||||||
} as Record<string, any>;
|
} as Record<string, any>;
|
||||||
|
|
||||||
if (info) {
|
if (info) {
|
||||||
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
|
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
|
||||||
updates.softwareVersion = info.software?.version;
|
updates.softwareVersion = info.software?.version;
|
||||||
@@ -100,27 +91,27 @@ export class FetchInstanceMetadataService {
|
|||||||
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
|
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
|
||||||
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
|
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name) updates.name = name;
|
if (name) updates.name = name;
|
||||||
if (description) updates.description = description;
|
if (description) updates.description = description;
|
||||||
if (icon || favicon) updates.iconUrl = icon ?? favicon;
|
if (icon || favicon) updates.iconUrl = icon ?? favicon;
|
||||||
if (favicon) updates.faviconUrl = favicon;
|
if (favicon) updates.faviconUrl = favicon;
|
||||||
if (themeColor) updates.themeColor = themeColor;
|
if (themeColor) updates.themeColor = themeColor;
|
||||||
|
|
||||||
await this.federatedInstanceService.update(instance.id, updates);
|
await this.federatedInstanceService.update(instance.id, updates);
|
||||||
|
|
||||||
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
|
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
|
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
|
||||||
} finally {
|
} finally {
|
||||||
await this.unlock(host);
|
unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
|
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
|
||||||
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -130,33 +121,33 @@ export class FetchInstanceMetadataService {
|
|||||||
throw err.statusCode ?? err.message;
|
throw err.statusCode ?? err.message;
|
||||||
}
|
}
|
||||||
}) as Record<string, unknown>;
|
}) as Record<string, unknown>;
|
||||||
|
|
||||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||||
throw new Error('No wellknown links');
|
throw new Error('No wellknown links');
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = wellknown.links as any[];
|
const links = wellknown.links as any[];
|
||||||
|
|
||||||
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||||
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||||
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
|
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
|
||||||
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
||||||
|
|
||||||
if (link == null) {
|
if (link == null) {
|
||||||
throw new Error('No nodeinfo link provided');
|
throw new Error('No nodeinfo link provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await this.httpRequestService.getJson(link.href)
|
const info = await this.httpRequestService.getJson(link.href)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
throw err.statusCode ?? err.message;
|
throw err.statusCode ?? err.message;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
|
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
|
||||||
|
|
||||||
return info as NodeInfo;
|
return info as NodeInfo;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
|
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,51 +155,51 @@ export class FetchInstanceMetadataService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
|
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
|
||||||
this.logger.info(`Fetching HTML of ${instance.host} ...`);
|
this.logger.info(`Fetching HTML of ${instance.host} ...`);
|
||||||
|
|
||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
|
|
||||||
const html = await this.httpRequestService.getHtml(url);
|
const html = await this.httpRequestService.getHtml(url);
|
||||||
|
|
||||||
const { window } = new JSDOM(html);
|
const { window } = new JSDOM(html);
|
||||||
const doc = window.document;
|
const doc = window.document;
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
|
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
|
||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
|
|
||||||
const manifestUrl = url + '/manifest.json';
|
const manifestUrl = url + '/manifest.json';
|
||||||
|
|
||||||
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
|
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
|
||||||
|
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
|
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
|
||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
||||||
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
|
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (new URL(href, url)).href;
|
return (new URL(href, url)).href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const faviconUrl = url + '/favicon.ico';
|
const faviconUrl = url + '/favicon.ico';
|
||||||
|
|
||||||
const favicon = await this.httpRequestService.send(faviconUrl, {
|
const favicon = await this.httpRequestService.send(faviconUrl, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
}, { throwErrorWhenResponseNotOk: false });
|
}, { throwErrorWhenResponseNotOk: false });
|
||||||
|
|
||||||
if (favicon.ok) {
|
if (favicon.ok) {
|
||||||
return faviconUrl;
|
return faviconUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,38 +209,38 @@ export class FetchInstanceMetadataService {
|
|||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
return (new URL(manifest.icons[0].src, url)).href;
|
return (new URL(manifest.icons[0].src, url)).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
|
|
||||||
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
||||||
const links = Array.from(doc.getElementsByTagName('link')).reverse();
|
const links = Array.from(doc.getElementsByTagName('link')).reverse();
|
||||||
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
|
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
|
||||||
const href =
|
const href =
|
||||||
[
|
[
|
||||||
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
|
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
|
||||||
links.find(link => link.relList.contains('apple-touch-icon'))?.href,
|
links.find(link => link.relList.contains('apple-touch-icon'))?.href,
|
||||||
links.find(link => link.relList.contains('icon'))?.href,
|
links.find(link => link.relList.contains('icon'))?.href,
|
||||||
]
|
]
|
||||||
.find(href => href);
|
.find(href => href);
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (new URL(href, url)).href;
|
return (new URL(href, url)).href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
|
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||||
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
|
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
|
||||||
|
|
||||||
if (themeColor) {
|
if (themeColor) {
|
||||||
const color = new tinycolor(themeColor);
|
const color = new tinycolor(themeColor);
|
||||||
if (color.isValid()) return color.toHexString();
|
if (color.isValid()) return color.toHexString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,19 +253,19 @@ export class FetchInstanceMetadataService {
|
|||||||
return info.metadata.name;
|
return info.metadata.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
|
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
|
||||||
|
|
||||||
if (og) {
|
if (og) {
|
||||||
return og;
|
return og;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
return manifest.name ?? manifest.short_name;
|
return manifest.name ?? manifest.short_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,23 +278,23 @@ export class FetchInstanceMetadataService {
|
|||||||
return info.metadata.description;
|
return info.metadata.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
|
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
|
||||||
if (meta) {
|
if (meta) {
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
|
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
|
||||||
if (og) {
|
if (og) {
|
||||||
return og;
|
return og;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
return manifest.name ?? manifest.short_name;
|
return manifest.name ?? manifest.short_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -161,20 +161,20 @@ export class FileInfoService {
|
|||||||
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
|
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
|
||||||
let sensitive = false;
|
let sensitive = false;
|
||||||
let porn = false;
|
let porn = false;
|
||||||
|
|
||||||
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
|
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
|
||||||
let sensitive = false;
|
let sensitive = false;
|
||||||
let porn = false;
|
let porn = false;
|
||||||
|
|
||||||
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||||
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||||
|
|
||||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
|
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
|
||||||
|
|
||||||
return [sensitive, porn];
|
return [sensitive, porn];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([
|
if ([
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
'image/png',
|
'image/png',
|
||||||
@@ -253,10 +253,10 @@ export class FileInfoService {
|
|||||||
disposeOutDir();
|
disposeOutDir();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [sensitive, porn];
|
return [sensitive, porn];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
|
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
|
||||||
const watcher = new FSWatcher({
|
const watcher = new FSWatcher({
|
||||||
cwd,
|
cwd,
|
||||||
@@ -295,7 +295,7 @@ export class FileInfoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private exists(path: string): Promise<boolean> {
|
private exists(path: string): Promise<boolean> {
|
||||||
return fs.promises.access(path).then(() => true, () => false);
|
return fs.promises.access(path).then(() => true, () => false);
|
||||||
@@ -304,11 +304,11 @@ export class FileInfoService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public fixMime(mime: string | fileType.MimeType): string {
|
public fixMime(mime: string | fileType.MimeType): string {
|
||||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||||
if (mime === 'audio/x-flac') {
|
if (mime === "audio/x-flac") {
|
||||||
return 'audio/flac';
|
return "audio/flac";
|
||||||
}
|
}
|
||||||
if (mime === 'audio/vnd.wave') {
|
if (mime === "audio/vnd.wave") {
|
||||||
return 'audio/wav';
|
return "audio/wav";
|
||||||
}
|
}
|
||||||
|
|
||||||
return mime;
|
return mime;
|
||||||
@@ -355,12 +355,11 @@ export class FileInfoService {
|
|||||||
* Check the file is SVG or not
|
* Check the file is SVG or not
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkSvg(path: string): Promise<boolean> {
|
public async checkSvg(path: string) {
|
||||||
try {
|
try {
|
||||||
const size = await this.getFileSize(path);
|
const size = await this.getFileSize(path);
|
||||||
if (size > 1 * 1024 * 1024) return false;
|
if (size > 1 * 1024 * 1024) return false;
|
||||||
const buffer = await fs.promises.readFile(path);
|
return isSvg(fs.readFileSync(path));
|
||||||
return isSvg(buffer.toString());
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { Role } from '@/models/index.js';
|
import { Role } from '@/models';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalEventService {
|
export class GlobalEventService {
|
||||||
|
@@ -42,21 +42,21 @@ export class HttpRequestService {
|
|||||||
errorTtl: 30, // 30secs
|
errorTtl: 30, // 30secs
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
this.http = new http.Agent({
|
this.http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup,
|
||||||
} as http.AgentOptions);
|
} as http.AgentOptions);
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
this.httpAgent = config.proxy
|
this.httpAgent = config.proxy
|
||||||
? new HttpProxyAgent({
|
? new HttpProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@@ -5,7 +5,7 @@ import type { Config } from '@/config.js';
|
|||||||
import { genAid, parseAid } from '@/misc/id/aid.js';
|
import { genAid, parseAid } from '@/misc/id/aid.js';
|
||||||
import { genMeid, parseMeid } from '@/misc/id/meid.js';
|
import { genMeid, parseMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId } from '@/misc/id/object-id.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { parseUlid } from '@/misc/id/ulid.js';
|
import { parseUlid } from '@/misc/id/ulid.js';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export class IdService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public genId(date?: Date): string {
|
public genId(date?: Date): string {
|
||||||
if (!date || (date > new Date())) date = new Date();
|
if (!date || (date > new Date())) date = new Date();
|
||||||
|
|
||||||
switch (this.method) {
|
switch (this.method) {
|
||||||
case 'aid': return genAid(date);
|
case 'aid': return genAid(date);
|
||||||
case 'meid': return genMeid(date);
|
case 'meid': return genMeid(date);
|
||||||
@@ -38,7 +38,7 @@ export class IdService {
|
|||||||
public parse(id: string): { date: Date; } {
|
public parse(id: string): { date: Date; } {
|
||||||
switch (this.method) {
|
switch (this.method) {
|
||||||
case 'aid': return parseAid(id);
|
case 'aid': return parseAid(id);
|
||||||
case 'objectid': return parseObjectId(id);
|
case 'objectid':
|
||||||
case 'meid': return parseMeid(id);
|
case 'meid': return parseMeid(id);
|
||||||
case 'meidg': return parseMeidg(id);
|
case 'meidg': return parseMeidg(id);
|
||||||
case 'ulid': return parseUlid(id);
|
case 'ulid': return parseUlid(id);
|
||||||
|
@@ -26,12 +26,12 @@ export class InstanceActorService {
|
|||||||
public async getInstanceActor(): Promise<LocalUser> {
|
public async getInstanceActor(): Promise<LocalUser> {
|
||||||
const cached = this.cache.get();
|
const cached = this.cache.get();
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
username: ACTOR_USERNAME,
|
username: ACTOR_USERNAME,
|
||||||
}) as LocalUser | undefined;
|
}) as LocalUser | undefined;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.cache.set(user);
|
this.cache.set(user);
|
||||||
return user;
|
return user;
|
||||||
|
@@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { KEYWORD } from 'color-convert/conversions.js';
|
import type { KEYWORD } from 'color-convert/conversions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggerService {
|
export class LoggerService {
|
||||||
|
@@ -56,7 +56,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(noCache = false): Promise<Meta> {
|
public async fetch(noCache = false): Promise<Meta> {
|
||||||
if (!noCache && this.cache) return this.cache;
|
if (!noCache && this.cache) return this.cache;
|
||||||
|
|
||||||
return await this.db.transaction(async transactionalEntityManager => {
|
return await this.db.transaction(async transactionalEntityManager => {
|
||||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||||
const metas = await transactionalEntityManager.find(Meta, {
|
const metas = await transactionalEntityManager.find(Meta, {
|
||||||
@@ -64,9 +64,9 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
id: 'DESC',
|
id: 'DESC',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = metas[0];
|
const meta = metas[0];
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
this.cache = meta;
|
this.cache = meta;
|
||||||
return meta;
|
return meta;
|
||||||
@@ -81,7 +81,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
['id'],
|
['id'],
|
||||||
)
|
)
|
||||||
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
|
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
|
||||||
|
|
||||||
this.cache = saved;
|
this.cache = saved;
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
@@ -27,29 +27,29 @@ export class MfmService {
|
|||||||
public fromHtml(html: string, hashtagNames?: string[]): string {
|
public fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
// some AP servers like Pixelfed use br tags as well as newlines
|
// some AP servers like Pixelfed use br tags as well as newlines
|
||||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||||
|
|
||||||
const dom = parse5.parseFragment(html);
|
const dom = parse5.parseFragment(html);
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|
||||||
for (const n of dom.childNodes) {
|
for (const n of dom.childNodes) {
|
||||||
analyze(n);
|
analyze(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.trim();
|
return text.trim();
|
||||||
|
|
||||||
function getText(node: TreeAdapter.Node): string {
|
function getText(node: TreeAdapter.Node): string {
|
||||||
if (treeAdapter.isTextNode(node)) return node.value;
|
if (treeAdapter.isTextNode(node)) return node.value;
|
||||||
if (!treeAdapter.isElementNode(node)) return '';
|
if (!treeAdapter.isElementNode(node)) return '';
|
||||||
if (node.nodeName === 'br') return '\n';
|
if (node.nodeName === 'br') return '\n';
|
||||||
|
|
||||||
if (node.childNodes) {
|
if (node.childNodes) {
|
||||||
return node.childNodes.map(n => getText(n)).join('');
|
return node.childNodes.map(n => getText(n)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||||
if (childNodes) {
|
if (childNodes) {
|
||||||
for (const n of childNodes) {
|
for (const n of childNodes) {
|
||||||
@@ -57,35 +57,35 @@ export class MfmService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyze(node: TreeAdapter.Node) {
|
function analyze(node: TreeAdapter.Node) {
|
||||||
if (treeAdapter.isTextNode(node)) {
|
if (treeAdapter.isTextNode(node)) {
|
||||||
text += node.value;
|
text += node.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip comment or document type node
|
// Skip comment or document type node
|
||||||
if (!treeAdapter.isElementNode(node)) return;
|
if (!treeAdapter.isElementNode(node)) return;
|
||||||
|
|
||||||
switch (node.nodeName) {
|
switch (node.nodeName) {
|
||||||
case 'br': {
|
case 'br': {
|
||||||
text += '\n';
|
text += '\n';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'a':
|
case 'a':
|
||||||
{
|
{
|
||||||
const txt = getText(node);
|
const txt = getText(node);
|
||||||
const rel = node.attrs.find(x => x.name === 'rel');
|
const rel = node.attrs.find(x => x.name === 'rel');
|
||||||
const href = node.attrs.find(x => x.name === 'href');
|
const href = node.attrs.find(x => x.name === 'href');
|
||||||
|
|
||||||
// ハッシュタグ
|
// ハッシュタグ
|
||||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||||
text += txt;
|
text += txt;
|
||||||
// メンション
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||||
const part = txt.split('@');
|
const part = txt.split('@');
|
||||||
|
|
||||||
if (part.length === 2 && href) {
|
if (part.length === 2 && href) {
|
||||||
//#region ホスト名部分が省略されているので復元する
|
//#region ホスト名部分が省略されているので復元する
|
||||||
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
||||||
@@ -116,12 +116,12 @@ export class MfmService {
|
|||||||
return `[${txt}](${href.value})`;
|
return `[${txt}](${href.value})`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
text += generateLink();
|
text += generateLink();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'h1':
|
case 'h1':
|
||||||
{
|
{
|
||||||
text += '【';
|
text += '【';
|
||||||
@@ -129,7 +129,7 @@ export class MfmService {
|
|||||||
text += '】\n';
|
text += '】\n';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'b':
|
case 'b':
|
||||||
case 'strong':
|
case 'strong':
|
||||||
{
|
{
|
||||||
@@ -138,7 +138,7 @@ export class MfmService {
|
|||||||
text += '**';
|
text += '**';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'small':
|
case 'small':
|
||||||
{
|
{
|
||||||
text += '<small>';
|
text += '<small>';
|
||||||
@@ -146,7 +146,7 @@ export class MfmService {
|
|||||||
text += '</small>';
|
text += '</small>';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 's':
|
case 's':
|
||||||
case 'del':
|
case 'del':
|
||||||
{
|
{
|
||||||
@@ -155,7 +155,7 @@ export class MfmService {
|
|||||||
text += '~~';
|
text += '~~';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'i':
|
case 'i':
|
||||||
case 'em':
|
case 'em':
|
||||||
{
|
{
|
||||||
@@ -164,7 +164,7 @@ export class MfmService {
|
|||||||
text += '</i>';
|
text += '</i>';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// block code (<pre><code>)
|
// block code (<pre><code>)
|
||||||
case 'pre': {
|
case 'pre': {
|
||||||
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
||||||
@@ -176,7 +176,7 @@ export class MfmService {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// inline code (<code>)
|
// inline code (<code>)
|
||||||
case 'code': {
|
case 'code': {
|
||||||
text += '`';
|
text += '`';
|
||||||
@@ -184,7 +184,7 @@ export class MfmService {
|
|||||||
text += '`';
|
text += '`';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'blockquote': {
|
case 'blockquote': {
|
||||||
const t = getText(node);
|
const t = getText(node);
|
||||||
if (t) {
|
if (t) {
|
||||||
@@ -193,7 +193,7 @@ export class MfmService {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'p':
|
case 'p':
|
||||||
case 'h2':
|
case 'h2':
|
||||||
case 'h3':
|
case 'h3':
|
||||||
@@ -205,7 +205,7 @@ export class MfmService {
|
|||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// other block elements
|
// other block elements
|
||||||
case 'div':
|
case 'div':
|
||||||
case 'header':
|
case 'header':
|
||||||
@@ -219,7 +219,7 @@ export class MfmService {
|
|||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: // includes inline elements
|
default: // includes inline elements
|
||||||
{
|
{
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
@@ -234,48 +234,48 @@ export class MfmService {
|
|||||||
if (nodes == null) {
|
if (nodes == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { window } = new Window();
|
const { window } = new Window();
|
||||||
|
|
||||||
const doc = window.document;
|
const doc = window.document;
|
||||||
|
|
||||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||||
if (children) {
|
if (children) {
|
||||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||||
bold: (node) => {
|
bold: (node) => {
|
||||||
const el = doc.createElement('b');
|
const el = doc.createElement('b');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
small: (node) => {
|
small: (node) => {
|
||||||
const el = doc.createElement('small');
|
const el = doc.createElement('small');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
strike: (node) => {
|
strike: (node) => {
|
||||||
const el = doc.createElement('del');
|
const el = doc.createElement('del');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
italic: (node) => {
|
italic: (node) => {
|
||||||
const el = doc.createElement('i');
|
const el = doc.createElement('i');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
fn: (node) => {
|
fn: (node) => {
|
||||||
const el = doc.createElement('i');
|
const el = doc.createElement('i');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
blockCode: (node) => {
|
blockCode: (node) => {
|
||||||
const pre = doc.createElement('pre');
|
const pre = doc.createElement('pre');
|
||||||
const inner = doc.createElement('code');
|
const inner = doc.createElement('code');
|
||||||
@@ -283,21 +283,21 @@ export class MfmService {
|
|||||||
pre.appendChild(inner);
|
pre.appendChild(inner);
|
||||||
return pre;
|
return pre;
|
||||||
},
|
},
|
||||||
|
|
||||||
center: (node) => {
|
center: (node) => {
|
||||||
const el = doc.createElement('div');
|
const el = doc.createElement('div');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
emojiCode: (node) => {
|
emojiCode: (node) => {
|
||||||
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
|
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
|
||||||
},
|
},
|
||||||
|
|
||||||
unicodeEmoji: (node) => {
|
unicodeEmoji: (node) => {
|
||||||
return doc.createTextNode(node.props.emoji);
|
return doc.createTextNode(node.props.emoji);
|
||||||
},
|
},
|
||||||
|
|
||||||
hashtag: (node) => {
|
hashtag: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
|
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
|
||||||
@@ -305,32 +305,32 @@ export class MfmService {
|
|||||||
a.setAttribute('rel', 'tag');
|
a.setAttribute('rel', 'tag');
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
inlineCode: (node) => {
|
inlineCode: (node) => {
|
||||||
const el = doc.createElement('code');
|
const el = doc.createElement('code');
|
||||||
el.textContent = node.props.code;
|
el.textContent = node.props.code;
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
mathInline: (node) => {
|
mathInline: (node) => {
|
||||||
const el = doc.createElement('code');
|
const el = doc.createElement('code');
|
||||||
el.textContent = node.props.formula;
|
el.textContent = node.props.formula;
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
mathBlock: (node) => {
|
mathBlock: (node) => {
|
||||||
const el = doc.createElement('code');
|
const el = doc.createElement('code');
|
||||||
el.textContent = node.props.formula;
|
el.textContent = node.props.formula;
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
link: (node) => {
|
link: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.setAttribute('href', node.props.url);
|
a.setAttribute('href', node.props.url);
|
||||||
appendChildren(node.children, a);
|
appendChildren(node.children, a);
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
mention: (node) => {
|
mention: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
const { username, host, acct } = node.props;
|
const { username, host, acct } = node.props;
|
||||||
@@ -340,47 +340,47 @@ export class MfmService {
|
|||||||
a.textContent = acct;
|
a.textContent = acct;
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
quote: (node) => {
|
quote: (node) => {
|
||||||
const el = doc.createElement('blockquote');
|
const el = doc.createElement('blockquote');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
text: (node) => {
|
text: (node) => {
|
||||||
const el = doc.createElement('span');
|
const el = doc.createElement('span');
|
||||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||||
|
|
||||||
for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
|
for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
|
||||||
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
||||||
}
|
}
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
url: (node) => {
|
url: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.setAttribute('href', node.props.url);
|
a.setAttribute('href', node.props.url);
|
||||||
a.textContent = node.props.url;
|
a.textContent = node.props.url;
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
search: (node) => {
|
search: (node) => {
|
||||||
const a = doc.createElement('a');
|
const a = doc.createElement('a');
|
||||||
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
|
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
|
||||||
a.textContent = node.props.content;
|
a.textContent = node.props.content;
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
|
|
||||||
plain: (node) => {
|
plain: (node) => {
|
||||||
const el = doc.createElement('span');
|
const el = doc.createElement('span');
|
||||||
appendChildren(node.children, el);
|
appendChildren(node.children, el);
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
appendChildren(nodes, doc.body);
|
appendChildren(nodes, doc.body);
|
||||||
|
|
||||||
return `<p>${doc.body.innerHTML}</p>`;
|
return `<p>${doc.body.innerHTML}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -672,7 +672,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
// Register to search database
|
// Register to search database
|
||||||
this.index(note);
|
this.index(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
|
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
|
||||||
if (sensitiveWord.length > 0) {
|
if (sensitiveWord.length > 0) {
|
||||||
@@ -758,7 +758,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private index(note: Note) {
|
private index(note: Note) {
|
||||||
if (note.text == null && note.cw == null) return;
|
if (note.text == null && note.cw == null) return;
|
||||||
|
|
||||||
this.searchService.indexNote(note);
|
this.searchService.indexNote(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -45,7 +45,7 @@ export class NoteDeleteService {
|
|||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 投稿を削除します。
|
* 投稿を削除します。
|
||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
@@ -121,8 +121,10 @@ export class NoteDeleteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async findCascadingNotes(note: Note): Promise<Note[]> {
|
private async findCascadingNotes(note: Note) {
|
||||||
const recursive = async (noteId: string): Promise<Note[]> => {
|
const cascadingNotes: Note[] = [];
|
||||||
|
|
||||||
|
const recursive = async (noteId: string) => {
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.replyId = :noteId', { noteId })
|
.where('note.replyId = :noteId', { noteId })
|
||||||
.orWhere(new Brackets(q => {
|
.orWhere(new Brackets(q => {
|
||||||
@@ -131,14 +133,12 @@ export class NoteDeleteService {
|
|||||||
}))
|
}))
|
||||||
.leftJoinAndSelect('note.user', 'user');
|
.leftJoinAndSelect('note.user', 'user');
|
||||||
const replies = await query.getMany();
|
const replies = await query.getMany();
|
||||||
|
for (const reply of replies) {
|
||||||
return [
|
cascadingNotes.push(reply);
|
||||||
replies,
|
await recursive(reply.id);
|
||||||
...await Promise.all(replies.map(reply => recursive(reply.id))),
|
}
|
||||||
].flat();
|
|
||||||
};
|
};
|
||||||
|
await recursive(note.id);
|
||||||
const cascadingNotes: Note[] = await recursive(note.id);
|
|
||||||
|
|
||||||
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
|
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
|
||||||
}
|
}
|
||||||
|
@@ -99,7 +99,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isMentioned: true,
|
isMentioned: true,
|
||||||
@@ -109,7 +109,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: true,
|
isSpecified: true,
|
||||||
|
@@ -46,7 +46,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
force = false,
|
force = false,
|
||||||
) {
|
) {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
|
|
||||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||||
`notificationTimeline:${userId}`,
|
`notificationTimeline:${userId}`,
|
||||||
'+',
|
'+',
|
||||||
|
@@ -39,12 +39,12 @@ export class PollService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async vote(user: User, note: Note, choice: number) {
|
public async vote(user: User, note: Note, choice: number) {
|
||||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
|
|
||||||
if (poll == null) throw new Error('poll not found');
|
if (poll == null) throw new Error('poll not found');
|
||||||
|
|
||||||
// Check whether is valid choice
|
// Check whether is valid choice
|
||||||
if (poll.choices[choice] == null) throw new Error('invalid choice param');
|
if (poll.choices[choice] == null) throw new Error('invalid choice param');
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
@@ -52,13 +52,13 @@ export class PollService {
|
|||||||
throw new Error('blocked');
|
throw new Error('blocked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already voted
|
// if already voted
|
||||||
const exist = await this.pollVotesRepository.findBy({
|
const exist = await this.pollVotesRepository.findBy({
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (poll.multiple) {
|
if (poll.multiple) {
|
||||||
if (exist.some(x => x.choice === choice)) {
|
if (exist.some(x => x.choice === choice)) {
|
||||||
throw new Error('already voted');
|
throw new Error('already voted');
|
||||||
@@ -66,7 +66,7 @@ export class PollService {
|
|||||||
} else if (exist.length !== 0) {
|
} else if (exist.length !== 0) {
|
||||||
throw new Error('already voted');
|
throw new Error('already voted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create vote
|
// Create vote
|
||||||
await this.pollVotesRepository.insert({
|
await this.pollVotesRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
@@ -75,11 +75,11 @@ export class PollService {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
choice: choice,
|
choice: choice,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Increment votes count
|
// Increment votes count
|
||||||
const index = choice + 1; // In SQL, array index is 1 based
|
const index = choice + 1; // In SQL, array index is 1 based
|
||||||
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
|
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
|
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
|
||||||
choice: choice,
|
choice: choice,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -90,10 +90,10 @@ export class PollService {
|
|||||||
public async deliverQuestionUpdate(noteId: Note['id']) {
|
public async deliverQuestionUpdate(noteId: Note['id']) {
|
||||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||||
if (note == null) throw new Error('note not found');
|
if (note == null) throw new Error('note not found');
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
if (user == null) throw new Error('note not found');
|
if (user == null) throw new Error('note not found');
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import push from 'web-push';
|
import push from 'web-push';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema';
|
||||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||||
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
|
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
@@ -31,7 +31,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
|
|||||||
...body.note,
|
...body.note,
|
||||||
// textをgetNoteSummaryしたものに置き換える
|
// textをgetNoteSummaryしたものに置き換える
|
||||||
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
|
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
|
||||||
|
|
||||||
cw: undefined,
|
cw: undefined,
|
||||||
reply: undefined,
|
reply: undefined,
|
||||||
renote: undefined,
|
renote: undefined,
|
||||||
@@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PushNotificationService implements OnApplicationShutdown {
|
export class PushNotificationService {
|
||||||
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
|
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -69,16 +69,16 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
|
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
|
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
|
||||||
|
|
||||||
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||||
push.setVapidDetails(this.config.url,
|
push.setVapidDetails(this.config.url,
|
||||||
meta.swPublicKey,
|
meta.swPublicKey,
|
||||||
meta.swPrivateKey);
|
meta.swPrivateKey);
|
||||||
|
|
||||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
if ([
|
if ([
|
||||||
'readAllNotifications',
|
'readAllNotifications',
|
||||||
@@ -103,7 +103,7 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||||||
//swLogger.info(err.statusCode);
|
//swLogger.info(err.statusCode);
|
||||||
//swLogger.info(err.headers);
|
//swLogger.info(err.headers);
|
||||||
//swLogger.info(err.body);
|
//swLogger.info(err.body);
|
||||||
|
|
||||||
if (err.statusCode === 410) {
|
if (err.statusCode === 410) {
|
||||||
this.swSubscriptionsRepository.delete({
|
this.swSubscriptionsRepository.delete({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -115,14 +115,4 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.subscriptionsCache.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -60,8 +60,8 @@ export class QueryService {
|
|||||||
q.orderBy(`${q.alias}.id`, 'DESC');
|
q.orderBy(`${q.alias}.id`, 'DESC');
|
||||||
}
|
}
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ここでいうBlockedは被Blockedの意
|
// ここでいうBlockedは被Blockedの意
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
|
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
|
||||||
@@ -109,18 +109,18 @@ export class QueryService {
|
|||||||
q.andWhere('note.channelId IS NULL');
|
q.andWhere('note.channelId IS NULL');
|
||||||
} else {
|
} else {
|
||||||
q.leftJoinAndSelect('note.channel', 'channel');
|
q.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
|
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
|
||||||
.select('channelFollowing.followeeId')
|
.select('channelFollowing.followeeId')
|
||||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
// チャンネルのノートではない
|
// チャンネルのノートではない
|
||||||
.where('note.channelId IS NULL')
|
.where('note.channelId IS NULL')
|
||||||
// または自分がフォローしているチャンネルのノート
|
// または自分がフォローしているチャンネルのノート
|
||||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(channelFollowingQuery.getParameters());
|
q.setParameters(channelFollowingQuery.getParameters());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,9 +130,9 @@ export class QueryService {
|
|||||||
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
|
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
|
||||||
.select('muted.noteId')
|
.select('muted.noteId')
|
||||||
.where('muted.userId = :userId', { userId: me.id });
|
.where('muted.userId = :userId', { userId: me.id });
|
||||||
|
|
||||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||||
|
|
||||||
q.setParameters(mutedQuery.getParameters());
|
q.setParameters(mutedQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,13 +141,13 @@ export class QueryService {
|
|||||||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||||
.select('threadMuted.threadId')
|
.select('threadMuted.threadId')
|
||||||
.where('threadMuted.userId = :userId', { userId: me.id });
|
.where('threadMuted.userId = :userId', { userId: me.id });
|
||||||
|
|
||||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
.where('note.threadId IS NULL')
|
.where('note.threadId IS NULL')
|
||||||
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
|
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(mutedQuery.getParameters());
|
q.setParameters(mutedQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +156,15 @@ export class QueryService {
|
|||||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
if (exclude) {
|
if (exclude) {
|
||||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||||
.select('user_profile.mutedInstances')
|
.select('user_profile.mutedInstances')
|
||||||
.where('user_profile.userId = :muterId', { muterId: me.id });
|
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
// 投稿の作者をミュートしていない かつ
|
// 投稿の作者をミュートしていない かつ
|
||||||
// 投稿の返信先の作者をミュートしていない かつ
|
// 投稿の返信先の作者をミュートしていない かつ
|
||||||
// 投稿の引用元の作者をミュートしていない
|
// 投稿の引用元の作者をミュートしていない
|
||||||
@@ -191,7 +191,7 @@ export class QueryService {
|
|||||||
.where('note.renoteUserHost IS NULL')
|
.where('note.renoteUserHost IS NULL')
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
q.setParameters(mutingInstanceQuery.getParameters());
|
q.setParameters(mutingInstanceQuery.getParameters());
|
||||||
}
|
}
|
||||||
@@ -201,9 +201,9 @@ export class QueryService {
|
|||||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export class QueryService {
|
|||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('following.followeeId')
|
.select('following.followeeId')
|
||||||
.where('following.followerId = :meId');
|
.where('following.followerId = :meId');
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
// 公開投稿である
|
// 公開投稿である
|
||||||
.where(new Brackets(qb => { qb
|
.where(new Brackets(qb => { qb
|
||||||
@@ -268,7 +268,7 @@ export class QueryService {
|
|||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters({ meId: me.id });
|
q.setParameters({ meId: me.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,10 +278,10 @@ export class QueryService {
|
|||||||
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
|
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
|
||||||
.select('renote_muting.muteeId')
|
.select('renote_muting.muteeId')
|
||||||
.where('renote_muting.muterId = :muterId', { muterId: me.id });
|
.where('renote_muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => {
|
q.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where(new Brackets(qb => {
|
.where(new Brackets(qb => {
|
||||||
qb.where('note.renoteId IS NOT NULL');
|
qb.where('note.renoteId IS NOT NULL');
|
||||||
qb.andWhere('note.text IS NULL');
|
qb.andWhere('note.text IS NULL');
|
||||||
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
@@ -289,7 +289,7 @@ export class QueryService {
|
|||||||
.orWhere('note.renoteId IS NULL')
|
.orWhere('note.renoteId IS NULL')
|
||||||
.orWhere('note.text IS NOT NULL');
|
.orWhere('note.text IS NOT NULL');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export class QueueService {
|
|||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
const data: DeliverJobData = {
|
const data = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
@@ -88,40 +88,6 @@ export class QueueService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい
|
|
||||||
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
|
|
||||||
* @param content IActivity | null
|
|
||||||
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
|
||||||
if (content == null) return null;
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
|
||||||
backoff: {
|
|
||||||
type: 'custom',
|
|
||||||
},
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
|
|
||||||
name: d[0],
|
|
||||||
data: {
|
|
||||||
user,
|
|
||||||
content,
|
|
||||||
to: d[0],
|
|
||||||
isSharedInbox: d[1],
|
|
||||||
} as DeliverJobData,
|
|
||||||
opts,
|
|
||||||
})));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||||
const data = {
|
const data = {
|
||||||
@@ -434,11 +400,11 @@ export class QueueService {
|
|||||||
this.deliverQueue.once('cleaned', (jobs, status) => {
|
this.deliverQueue.once('cleaned', (jobs, status) => {
|
||||||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||||
});
|
});
|
||||||
this.deliverQueue.clean(0, 0, 'delayed');
|
this.deliverQueue.clean(0, Infinity, 'delayed');
|
||||||
|
|
||||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||||
});
|
});
|
||||||
this.inboxQueue.clean(0, 0, 'delayed');
|
this.inboxQueue.clean(0, Infinity, 'delayed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -39,9 +39,9 @@ export class RelayService {
|
|||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
username: ACTOR_USERNAME,
|
username: ACTOR_USERNAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) return user as LocalUser;
|
if (user) return user as LocalUser;
|
||||||
|
|
||||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
|
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
|
||||||
return created as LocalUser;
|
return created as LocalUser;
|
||||||
}
|
}
|
||||||
@@ -53,12 +53,12 @@ export class RelayService {
|
|||||||
inbox,
|
inbox,
|
||||||
status: 'requesting',
|
status: 'requesting',
|
||||||
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
const relayActor = await this.getRelayActor();
|
const relayActor = await this.getRelayActor();
|
||||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||||
const activity = this.apRendererService.addContext(follow);
|
const activity = this.apRendererService.addContext(follow);
|
||||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||||
|
|
||||||
return relay;
|
return relay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,17 +67,17 @@ export class RelayService {
|
|||||||
const relay = await this.relaysRepository.findOneBy({
|
const relay = await this.relaysRepository.findOneBy({
|
||||||
inbox,
|
inbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (relay == null) {
|
if (relay == null) {
|
||||||
throw new Error('relay not found');
|
throw new Error('relay not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayActor = await this.getRelayActor();
|
const relayActor = await this.getRelayActor();
|
||||||
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||||
const undo = this.apRendererService.renderUndo(follow, relayActor);
|
const undo = this.apRendererService.renderUndo(follow, relayActor);
|
||||||
const activity = this.apRendererService.addContext(undo);
|
const activity = this.apRendererService.addContext(undo);
|
||||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||||
|
|
||||||
await this.relaysRepository.delete(relay.id);
|
await this.relaysRepository.delete(relay.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ export class RelayService {
|
|||||||
const relays = await this.relaysRepository.find();
|
const relays = await this.relaysRepository.find();
|
||||||
return relays;
|
return relays;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async relayAccepted(id: string): Promise<string> {
|
public async relayAccepted(id: string): Promise<string> {
|
||||||
const result = await this.relaysRepository.update(id, {
|
const result = await this.relaysRepository.update(id, {
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
});
|
});
|
||||||
|
|
||||||
return JSON.stringify(result);
|
return JSON.stringify(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,24 +101,24 @@ export class RelayService {
|
|||||||
const result = await this.relaysRepository.update(id, {
|
const result = await this.relaysRepository.update(id, {
|
||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
});
|
});
|
||||||
|
|
||||||
return JSON.stringify(result);
|
return JSON.stringify(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||||
if (activity == null) return;
|
if (activity == null) return;
|
||||||
|
|
||||||
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
|
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
}));
|
}));
|
||||||
if (relays.length === 0) return;
|
if (relays.length === 0) return;
|
||||||
|
|
||||||
const copy = deepClone(activity);
|
const copy = deepClone(activity);
|
||||||
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
|
|
||||||
const signed = await this.apRendererService.attachLdSignature(copy, user);
|
const signed = await this.apRendererService.attachLdSignature(copy, user);
|
||||||
|
|
||||||
for (const relay of relays) {
|
for (const relay of relays) {
|
||||||
this.queueService.deliver(user, signed, relay.inbox, false);
|
this.queueService.deliver(user, signed, relay.inbox, false);
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@ export class RemoteUserResolveService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
|
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
this.logger.info(`return local user: ${usernameLower}`);
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||||
@@ -46,9 +46,9 @@ export class RemoteUserResolveService {
|
|||||||
}
|
}
|
||||||
}) as LocalUser;
|
}) as LocalUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
if (this.config.host === host) {
|
if (this.config.host === host) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
this.logger.info(`return local user: ${usernameLower}`);
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||||
@@ -59,39 +59,39 @@ export class RemoteUserResolveService {
|
|||||||
}
|
}
|
||||||
}) as LocalUser;
|
}) as LocalUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
||||||
|
|
||||||
const acctLower = `${usernameLower}@${host}`;
|
const acctLower = `${usernameLower}@${host}`;
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
const self = await this.resolveSelf(acctLower);
|
const self = await this.resolveSelf(acctLower);
|
||||||
|
|
||||||
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
||||||
return await this.apPersonService.createPerson(self.href);
|
return await this.apPersonService.createPerson(self.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
||||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`try resync: ${acctLower}`);
|
this.logger.info(`try resync: ${acctLower}`);
|
||||||
const self = await this.resolveSelf(acctLower);
|
const self = await this.resolveSelf(acctLower);
|
||||||
|
|
||||||
if (user.uri !== self.href) {
|
if (user.uri !== self.href) {
|
||||||
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
||||||
this.logger.info(`uri missmatch: ${acctLower}`);
|
this.logger.info(`uri missmatch: ${acctLower}`);
|
||||||
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
|
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
|
||||||
|
|
||||||
// validate uri
|
// validate uri
|
||||||
const uri = new URL(self.href);
|
const uri = new URL(self.href);
|
||||||
if (uri.hostname !== host) {
|
if (uri.hostname !== host) {
|
||||||
throw new Error('Invalid uri');
|
throw new Error('Invalid uri');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.usersRepository.update({
|
await this.usersRepository.update({
|
||||||
usernameLower,
|
usernameLower,
|
||||||
host: host,
|
host: host,
|
||||||
@@ -101,9 +101,9 @@ export class RemoteUserResolveService {
|
|||||||
} else {
|
} else {
|
||||||
this.logger.info(`uri is fine: ${acctLower}`);
|
this.logger.info(`uri is fine: ${acctLower}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.apPersonService.updatePerson(self.href);
|
await this.apPersonService.updatePerson(self.href);
|
||||||
|
|
||||||
this.logger.info(`return resynced remote user: ${acctLower}`);
|
this.logger.info(`return resynced remote user: ${acctLower}`);
|
||||||
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
|
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
@@ -113,7 +113,7 @@ export class RemoteUserResolveService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`return existing remote user: ${acctLower}`);
|
this.logger.info(`return existing remote user: ${acctLower}`);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
@@ -392,7 +392,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
|
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
throw new RoleService.NotAssignedError();
|
throw new RoleService.NotAssignedError();
|
||||||
@@ -435,7 +435,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
this.roleAssignmentByUserIdCache.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -174,7 +174,7 @@ export class SearchService {
|
|||||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
|
||||||
return await query.limit(pagination.limit).getMany();
|
return await query.take(pagination.limit).getMany();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -50,31 +50,31 @@ export class SignupService {
|
|||||||
}) {
|
}) {
|
||||||
const { username, password, passwordHash, host } = opts;
|
const { username, password, passwordHash, host } = opts;
|
||||||
let hash = passwordHash;
|
let hash = passwordHash;
|
||||||
|
|
||||||
// Validate username
|
// Validate username
|
||||||
if (!this.userEntityService.validateLocalUsername(username)) {
|
if (!this.userEntityService.validateLocalUsername(username)) {
|
||||||
throw new Error('INVALID_USERNAME');
|
throw new Error('INVALID_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password != null && passwordHash == null) {
|
if (password != null && passwordHash == null) {
|
||||||
// Validate password
|
// Validate password
|
||||||
if (!this.userEntityService.validatePassword(password)) {
|
if (!this.userEntityService.validatePassword(password)) {
|
||||||
throw new Error('INVALID_PASSWORD');
|
throw new Error('INVALID_PASSWORD');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = await bcrypt.genSalt(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
hash = await bcrypt.hash(password, salt);
|
hash = await bcrypt.hash(password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateUserToken();
|
const secret = generateUserToken();
|
||||||
|
|
||||||
// Check username duplication
|
// Check username duplication
|
||||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||||
throw new Error('DUPLICATED_USERNAME');
|
throw new Error('DUPLICATED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
@@ -106,18 +106,18 @@ export class SignupService {
|
|||||||
}, (err, publicKey, privateKey) =>
|
}, (err, publicKey, privateKey) =>
|
||||||
err ? rej(err) : res([publicKey, privateKey]),
|
err ? rej(err) : res([publicKey, privateKey]),
|
||||||
));
|
));
|
||||||
|
|
||||||
let account!: User;
|
let account!: User;
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
const exist = await transactionalEntityManager.findOneBy(User, {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) throw new Error(' the username is already used');
|
if (exist) throw new Error(' the username is already used');
|
||||||
|
|
||||||
account = await transactionalEntityManager.save(new User({
|
account = await transactionalEntityManager.save(new User({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -127,27 +127,27 @@ export class SignupService {
|
|||||||
token: secret,
|
token: secret,
|
||||||
isRoot: isTheFirstUser,
|
isRoot: isTheFirstUser,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UserKeypair({
|
await transactionalEntityManager.save(new UserKeypair({
|
||||||
publicKey: keyPair[0],
|
publicKey: keyPair[0],
|
||||||
privateKey: keyPair[1],
|
privateKey: keyPair[1],
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
autoAcceptFollowed: true,
|
autoAcceptFollowed: true,
|
||||||
password: hash,
|
password: hash,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UsedUsername({
|
await transactionalEntityManager.save(new UsedUsername({
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
username: username.toLowerCase(),
|
username: username.toLowerCase(),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.usersChart.update(account, true);
|
this.usersChart.update(account, true);
|
||||||
|
|
||||||
return { account, secret };
|
return { account, secret };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,7 +69,7 @@ function verifyCertificateChain(certificates: string[]) {
|
|||||||
|
|
||||||
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
|
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
|
||||||
if (certStruct == null) throw new Error('certStruct is null');
|
if (certStruct == null) throw new Error('certStruct is null');
|
||||||
|
|
||||||
const algorithm = certificate.getSignatureAlgorithmField();
|
const algorithm = certificate.getSignatureAlgorithmField();
|
||||||
const signatureHex = certificate.getSignatureValueHex();
|
const signatureHex = certificate.getSignatureValueHex();
|
||||||
|
|
||||||
@@ -143,19 +143,19 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (clientData.type !== 'webauthn.get') {
|
if (clientData.type !== 'webauthn.get') {
|
||||||
throw new Error('type is not webauthn.get');
|
throw new Error('type is not webauthn.get');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hash(clientData.challenge).toString('hex') !== challenge) {
|
if (this.hash(clientData.challenge).toString('hex') !== challenge) {
|
||||||
throw new Error('challenge mismatch');
|
throw new Error('challenge mismatch');
|
||||||
}
|
}
|
||||||
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
|
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
|
||||||
throw new Error('origin mismatch');
|
throw new Error('origin mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationData = Buffer.concat(
|
const verificationData = Buffer.concat(
|
||||||
[authenticatorData, this.hash(clientDataJSON)],
|
[authenticatorData, this.hash(clientDataJSON)],
|
||||||
32 + authenticatorData.length,
|
32 + authenticatorData.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
return crypto
|
return crypto
|
||||||
.createVerify('SHA256')
|
.createVerify('SHA256')
|
||||||
.update(verificationData)
|
.update(verificationData)
|
||||||
@@ -168,7 +168,7 @@ export class TwoFactorAuthenticationService {
|
|||||||
none: {
|
none: {
|
||||||
verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
|
verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
|
||||||
const negTwo = publicKey.get(-2);
|
const negTwo = publicKey.get(-2);
|
||||||
|
|
||||||
if (!negTwo || negTwo.length !== 32) {
|
if (!negTwo || negTwo.length !== 32) {
|
||||||
throw new Error('invalid or no -2 key given');
|
throw new Error('invalid or no -2 key given');
|
||||||
}
|
}
|
||||||
@@ -176,12 +176,12 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (!negThree || negThree.length !== 32) {
|
if (!negThree || negThree.length !== 32) {
|
||||||
throw new Error('invalid or no -3 key given');
|
throw new Error('invalid or no -3 key given');
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyU2F = Buffer.concat(
|
const publicKeyU2F = Buffer.concat(
|
||||||
[ECC_PRELUDE, negTwo, negThree],
|
[ECC_PRELUDE, negTwo, negThree],
|
||||||
1 + 32 + 32,
|
1 + 32 + 32,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: publicKeyU2F,
|
publicKey: publicKeyU2F,
|
||||||
valid: true,
|
valid: true,
|
||||||
@@ -207,16 +207,16 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (attStmt.alg !== -7) {
|
if (attStmt.alg !== -7) {
|
||||||
throw new Error('alg mismatch');
|
throw new Error('alg mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationData = Buffer.concat([
|
const verificationData = Buffer.concat([
|
||||||
authenticatorData,
|
authenticatorData,
|
||||||
clientDataHash,
|
clientDataHash,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const attCert: Buffer = attStmt.x5c[0];
|
const attCert: Buffer = attStmt.x5c[0];
|
||||||
|
|
||||||
const negTwo = publicKey.get(-2);
|
const negTwo = publicKey.get(-2);
|
||||||
|
|
||||||
if (!negTwo || negTwo.length !== 32) {
|
if (!negTwo || negTwo.length !== 32) {
|
||||||
throw new Error('invalid or no -2 key given');
|
throw new Error('invalid or no -2 key given');
|
||||||
}
|
}
|
||||||
@@ -224,23 +224,23 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (!negThree || negThree.length !== 32) {
|
if (!negThree || negThree.length !== 32) {
|
||||||
throw new Error('invalid or no -3 key given');
|
throw new Error('invalid or no -3 key given');
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyData = Buffer.concat(
|
const publicKeyData = Buffer.concat(
|
||||||
[ECC_PRELUDE, negTwo, negThree],
|
[ECC_PRELUDE, negTwo, negThree],
|
||||||
1 + 32 + 32,
|
1 + 32 + 32,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!attCert.equals(publicKeyData)) {
|
if (!attCert.equals(publicKeyData)) {
|
||||||
throw new Error('public key mismatch');
|
throw new Error('public key mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = crypto
|
const isValid = crypto
|
||||||
.createVerify('SHA256')
|
.createVerify('SHA256')
|
||||||
.update(verificationData)
|
.update(verificationData)
|
||||||
.verify(PEMString(attCert), attStmt.sig);
|
.verify(PEMString(attCert), attStmt.sig);
|
||||||
|
|
||||||
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: isValid,
|
valid: isValid,
|
||||||
publicKey: publicKeyData,
|
publicKey: publicKeyData,
|
||||||
@@ -267,43 +267,43 @@ export class TwoFactorAuthenticationService {
|
|||||||
const verificationData = this.hash(
|
const verificationData = this.hash(
|
||||||
Buffer.concat([authenticatorData, clientDataHash]),
|
Buffer.concat([authenticatorData, clientDataHash]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const jwsParts = attStmt.response.toString('utf-8').split('.');
|
const jwsParts = attStmt.response.toString('utf-8').split('.');
|
||||||
|
|
||||||
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
|
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
|
||||||
const response = JSON.parse(
|
const response = JSON.parse(
|
||||||
base64URLDecode(jwsParts[1]).toString('utf-8'),
|
base64URLDecode(jwsParts[1]).toString('utf-8'),
|
||||||
);
|
);
|
||||||
const signature = jwsParts[2];
|
const signature = jwsParts[2];
|
||||||
|
|
||||||
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
|
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
|
||||||
throw new Error('invalid nonce');
|
throw new Error('invalid nonce');
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateChain = header.x5c
|
const certificateChain = header.x5c
|
||||||
.map((key: any) => PEMString(key))
|
.map((key: any) => PEMString(key))
|
||||||
.concat([GSR2]);
|
.concat([GSR2]);
|
||||||
|
|
||||||
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
|
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
|
||||||
throw new Error('invalid common name');
|
throw new Error('invalid common name');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyCertificateChain(certificateChain)) {
|
if (!verifyCertificateChain(certificateChain)) {
|
||||||
throw new Error('Invalid certificate chain!');
|
throw new Error('Invalid certificate chain!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const signatureBase = Buffer.from(
|
const signatureBase = Buffer.from(
|
||||||
jwsParts[0] + '.' + jwsParts[1],
|
jwsParts[0] + '.' + jwsParts[1],
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
const valid = crypto
|
const valid = crypto
|
||||||
.createVerify('sha256')
|
.createVerify('sha256')
|
||||||
.update(signatureBase)
|
.update(signatureBase)
|
||||||
.verify(certificateChain[0], base64URLDecode(signature));
|
.verify(certificateChain[0], base64URLDecode(signature));
|
||||||
|
|
||||||
const negTwo = publicKey.get(-2);
|
const negTwo = publicKey.get(-2);
|
||||||
|
|
||||||
if (!negTwo || negTwo.length !== 32) {
|
if (!negTwo || negTwo.length !== 32) {
|
||||||
throw new Error('invalid or no -2 key given');
|
throw new Error('invalid or no -2 key given');
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (!negThree || negThree.length !== 32) {
|
if (!negThree || negThree.length !== 32) {
|
||||||
throw new Error('invalid or no -3 key given');
|
throw new Error('invalid or no -3 key given');
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyData = Buffer.concat(
|
const publicKeyData = Buffer.concat(
|
||||||
[ECC_PRELUDE, negTwo, negThree],
|
[ECC_PRELUDE, negTwo, negThree],
|
||||||
1 + 32 + 32,
|
1 + 32 + 32,
|
||||||
@@ -342,17 +342,17 @@ export class TwoFactorAuthenticationService {
|
|||||||
authenticatorData,
|
authenticatorData,
|
||||||
clientDataHash,
|
clientDataHash,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (attStmt.x5c) {
|
if (attStmt.x5c) {
|
||||||
const attCert = attStmt.x5c[0];
|
const attCert = attStmt.x5c[0];
|
||||||
|
|
||||||
const validSignature = crypto
|
const validSignature = crypto
|
||||||
.createVerify('SHA256')
|
.createVerify('SHA256')
|
||||||
.update(verificationData)
|
.update(verificationData)
|
||||||
.verify(PEMString(attCert), attStmt.sig);
|
.verify(PEMString(attCert), attStmt.sig);
|
||||||
|
|
||||||
const negTwo = publicKey.get(-2);
|
const negTwo = publicKey.get(-2);
|
||||||
|
|
||||||
if (!negTwo || negTwo.length !== 32) {
|
if (!negTwo || negTwo.length !== 32) {
|
||||||
throw new Error('invalid or no -2 key given');
|
throw new Error('invalid or no -2 key given');
|
||||||
}
|
}
|
||||||
@@ -360,12 +360,12 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (!negThree || negThree.length !== 32) {
|
if (!negThree || negThree.length !== 32) {
|
||||||
throw new Error('invalid or no -3 key given');
|
throw new Error('invalid or no -3 key given');
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyData = Buffer.concat(
|
const publicKeyData = Buffer.concat(
|
||||||
[ECC_PRELUDE, negTwo, negThree],
|
[ECC_PRELUDE, negTwo, negThree],
|
||||||
1 + 32 + 32,
|
1 + 32 + 32,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: validSignature,
|
valid: validSignature,
|
||||||
publicKey: publicKeyData,
|
publicKey: publicKeyData,
|
||||||
@@ -375,12 +375,12 @@ export class TwoFactorAuthenticationService {
|
|||||||
throw new Error('ECDAA-Verify is not supported');
|
throw new Error('ECDAA-Verify is not supported');
|
||||||
} else {
|
} else {
|
||||||
if (attStmt.alg !== -7) throw new Error('alg mismatch');
|
if (attStmt.alg !== -7) throw new Error('alg mismatch');
|
||||||
|
|
||||||
throw new Error('self attestation is not supported');
|
throw new Error('self attestation is not supported');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'fido-u2f': {
|
'fido-u2f': {
|
||||||
verify({
|
verify({
|
||||||
attStmt,
|
attStmt,
|
||||||
@@ -401,13 +401,13 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (x5c.length !== 1) {
|
if (x5c.length !== 1) {
|
||||||
throw new Error('x5c length does not match expectation');
|
throw new Error('x5c length does not match expectation');
|
||||||
}
|
}
|
||||||
|
|
||||||
const attCert = x5c[0];
|
const attCert = x5c[0];
|
||||||
|
|
||||||
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
||||||
|
|
||||||
const negTwo: Buffer = publicKey.get(-2);
|
const negTwo: Buffer = publicKey.get(-2);
|
||||||
|
|
||||||
if (!negTwo || negTwo.length !== 32) {
|
if (!negTwo || negTwo.length !== 32) {
|
||||||
throw new Error('invalid or no -2 key given');
|
throw new Error('invalid or no -2 key given');
|
||||||
}
|
}
|
||||||
@@ -415,12 +415,12 @@ export class TwoFactorAuthenticationService {
|
|||||||
if (!negThree || negThree.length !== 32) {
|
if (!negThree || negThree.length !== 32) {
|
||||||
throw new Error('invalid or no -3 key given');
|
throw new Error('invalid or no -3 key given');
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyU2F = Buffer.concat(
|
const publicKeyU2F = Buffer.concat(
|
||||||
[ECC_PRELUDE, negTwo, negThree],
|
[ECC_PRELUDE, negTwo, negThree],
|
||||||
1 + 32 + 32,
|
1 + 32 + 32,
|
||||||
);
|
);
|
||||||
|
|
||||||
const verificationData = Buffer.concat([
|
const verificationData = Buffer.concat([
|
||||||
NULL_BYTE,
|
NULL_BYTE,
|
||||||
rpIdHash,
|
rpIdHash,
|
||||||
@@ -428,12 +428,12 @@ export class TwoFactorAuthenticationService {
|
|||||||
credentialId,
|
credentialId,
|
||||||
publicKeyU2F,
|
publicKeyU2F,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const validSignature = crypto
|
const validSignature = crypto
|
||||||
.createVerify('SHA256')
|
.createVerify('SHA256')
|
||||||
.update(verificationData)
|
.update(verificationData)
|
||||||
.verify(PEMString(attCert), attStmt.sig);
|
.verify(PEMString(attCert), attStmt.sig);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: validSignature,
|
valid: validSignature,
|
||||||
publicKey: publicKeyU2F,
|
publicKey: publicKeyU2F,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { UserKeypairsRepository } from '@/models/index.js';
|
import type { UserKeypairsRepository } from '@/models/index.js';
|
||||||
@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserKeypairService implements OnApplicationShutdown {
|
export class UserKeypairService {
|
||||||
private cache: RedisKVCache<UserKeypair>;
|
private cache: RedisKVCache<UserKeypair>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -31,14 +31,4 @@ export class UserKeypairService implements OnApplicationShutdown {
|
|||||||
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||||
return await this.cache.fetch(userId);
|
return await this.cache.fetch(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.cache.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -32,13 +32,13 @@ export class UserSuspendService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
|
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
|
||||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにDelete配信
|
// 知り得る全SharedInboxにDelete配信
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
|
|
||||||
const followings = await this.followingsRepository.find({
|
const followings = await this.followingsRepository.find({
|
||||||
where: [
|
where: [
|
||||||
{ followerSharedInbox: Not(IsNull()) },
|
{ followerSharedInbox: Not(IsNull()) },
|
||||||
@@ -46,13 +46,13 @@ export class UserSuspendService {
|
|||||||
],
|
],
|
||||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||||
|
|
||||||
for (const inbox of inboxes) {
|
for (const inbox of inboxes) {
|
||||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const inbox of queue) {
|
for (const inbox of queue) {
|
||||||
this.queueService.deliver(user, content, inbox, true);
|
this.queueService.deliver(user, content, inbox, true);
|
||||||
}
|
}
|
||||||
@@ -62,13 +62,13 @@ export class UserSuspendService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async doPostUnsuspend(user: User): Promise<void> {
|
public async doPostUnsuspend(user: User): Promise<void> {
|
||||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにUndo Delete配信
|
// 知り得る全SharedInboxにUndo Delete配信
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
|
|
||||||
const followings = await this.followingsRepository.find({
|
const followings = await this.followingsRepository.find({
|
||||||
where: [
|
where: [
|
||||||
{ followerSharedInbox: Not(IsNull()) },
|
{ followerSharedInbox: Not(IsNull()) },
|
||||||
@@ -76,13 +76,13 @@ export class UserSuspendService {
|
|||||||
],
|
],
|
||||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||||
|
|
||||||
for (const inbox of inboxes) {
|
for (const inbox of inboxes) {
|
||||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const inbox of queue) {
|
for (const inbox of queue) {
|
||||||
this.queueService.deliver(user as any, content, inbox, true);
|
this.queueService.deliver(user as any, content, inbox, true);
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ export class VideoProcessingService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async generateVideoThumbnail(source: string): Promise<IImage> {
|
public async generateVideoThumbnail(source: string): Promise<IImage> {
|
||||||
const [dir, cleanup] = await createTempDir();
|
const [dir, cleanup] = await createTempDir();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((res, rej) => {
|
await new Promise((res, rej) => {
|
||||||
FFmpeg({
|
FFmpeg({
|
||||||
|
@@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
this.webhooksFetched = true;
|
this.webhooksFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.webhooks;
|
return this.webhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,14 +27,14 @@ export class ApAudienceService {
|
|||||||
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||||
const toGroups = this.groupingAudience(getApIds(to), actor);
|
const toGroups = this.groupingAudience(getApIds(to), actor);
|
||||||
const ccGroups = this.groupingAudience(getApIds(cc), actor);
|
const ccGroups = this.groupingAudience(getApIds(cc), actor);
|
||||||
|
|
||||||
const others = unique(concat([toGroups.other, ccGroups.other]));
|
const others = unique(concat([toGroups.other, ccGroups.other]));
|
||||||
|
|
||||||
const limit = promiseLimit<User | null>(2);
|
const limit = promiseLimit<User | null>(2);
|
||||||
const mentionedUsers = (await Promise.all(
|
const mentionedUsers = (await Promise.all(
|
||||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||||
)).filter((x): x is User => x != null);
|
)).filter((x): x is User => x != null);
|
||||||
|
|
||||||
if (toGroups.public.length > 0) {
|
if (toGroups.public.length > 0) {
|
||||||
return {
|
return {
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
@@ -42,7 +42,7 @@ export class ApAudienceService {
|
|||||||
visibleUsers: [],
|
visibleUsers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ccGroups.public.length > 0) {
|
if (ccGroups.public.length > 0) {
|
||||||
return {
|
return {
|
||||||
visibility: 'home',
|
visibility: 'home',
|
||||||
@@ -50,7 +50,7 @@ export class ApAudienceService {
|
|||||||
visibleUsers: [],
|
visibleUsers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toGroups.followers.length > 0) {
|
if (toGroups.followers.length > 0) {
|
||||||
return {
|
return {
|
||||||
visibility: 'followers',
|
visibility: 'followers',
|
||||||
@@ -58,14 +58,14 @@ export class ApAudienceService {
|
|||||||
visibleUsers: [],
|
visibleUsers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visibility: 'specified',
|
visibility: 'specified',
|
||||||
mentionedUsers,
|
mentionedUsers,
|
||||||
visibleUsers: mentionedUsers,
|
visibleUsers: mentionedUsers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
private groupingAudience(ids: string[], actor: RemoteUser) {
|
||||||
const groups = {
|
const groups = {
|
||||||
@@ -73,7 +73,7 @@ export class ApAudienceService {
|
|||||||
followers: [] as string[],
|
followers: [] as string[],
|
||||||
other: [] as string[],
|
other: [] as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
if (this.isPublic(id)) {
|
if (this.isPublic(id)) {
|
||||||
groups.public.push(id);
|
groups.public.push(id);
|
||||||
@@ -83,12 +83,12 @@ export class ApAudienceService {
|
|||||||
groups.other.push(id);
|
groups.other.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.other = unique(groups.other);
|
groups.other = unique(groups.other);
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isPublic(id: string) {
|
private isPublic(id: string) {
|
||||||
return [
|
return [
|
||||||
@@ -97,7 +97,7 @@ export class ApAudienceService {
|
|||||||
'Public',
|
'Public',
|
||||||
].includes(id);
|
].includes(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isFollowers(id: string, actor: RemoteUser) {
|
private isFollowers(id: string, actor: RemoteUser) {
|
||||||
return (
|
return (
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import escapeRegexp from 'escape-regexp';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@@ -29,7 +30,7 @@ export type UriParseResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApDbResolverService implements OnApplicationShutdown {
|
export class ApDbResolverService {
|
||||||
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
||||||
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
||||||
|
|
||||||
@@ -55,18 +56,25 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public parseUri(value: string | IObject): UriParseResult {
|
public parseUri(value: string | IObject): UriParseResult {
|
||||||
const separator = '/';
|
const uri = getApId(value);
|
||||||
|
|
||||||
const uri = new URL(getApId(value));
|
// the host part of a URL is case insensitive, so use the 'i' flag.
|
||||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
|
||||||
|
const matchLocal = uri.match(localRegex);
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
|
||||||
return {
|
if (matchLocal) {
|
||||||
local: true,
|
return {
|
||||||
type,
|
local: true,
|
||||||
id,
|
type: matchLocal[1],
|
||||||
rest: rest.length === 0 ? undefined : rest.join(separator),
|
id: matchLocal[2],
|
||||||
};
|
rest: matchLocal[3],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
local: false,
|
||||||
|
uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,7 +129,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||||||
const key = await this.userPublickeysRepository.findOneBy({
|
const key = await this.userPublickeysRepository.findOneBy({
|
||||||
keyId,
|
keyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
@@ -147,22 +155,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
|
|
||||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.publicKeyCache.dispose();
|
|
||||||
this.publicKeyByUserIdCache.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -7,8 +7,6 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
|||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
|
||||||
import { ThinUser } from '@/queue/types.js';
|
|
||||||
|
|
||||||
interface IRecipe {
|
interface IRecipe {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -23,10 +21,10 @@ interface IDirectRecipe extends IRecipe {
|
|||||||
to: RemoteUser;
|
to: RemoteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||||
recipe.type === 'Followers';
|
recipe.type === 'Followers';
|
||||||
|
|
||||||
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
const isDirect = (recipe: any): recipe is IDirectRecipe =>
|
||||||
recipe.type === 'Direct';
|
recipe.type === 'Direct';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -48,11 +46,11 @@ export class ApDeliverManagerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to followers
|
* Deliver activity to followers
|
||||||
* @param actor
|
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
|
* @param from Followee
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
@@ -66,12 +64,11 @@ export class ApDeliverManagerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to user
|
* Deliver activity to user
|
||||||
* @param actor
|
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
* @param to Target user
|
* @param to Target user
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
@@ -84,28 +81,25 @@ export class ApDeliverManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) {
|
||||||
return new DeliverManager(
|
return new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
this.queueService,
|
this.queueService,
|
||||||
|
|
||||||
actor,
|
actor,
|
||||||
activity,
|
activity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeliverManager {
|
class DeliverManager {
|
||||||
private actor: ThinUser;
|
private actor: { id: User['id']; host: null; };
|
||||||
private activity: IActivity | null;
|
private activity: any;
|
||||||
private recipes: IRecipe[] = [];
|
private recipes: IRecipe[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param userEntityService
|
|
||||||
* @param followingsRepository
|
|
||||||
* @param queueService
|
|
||||||
* @param actor Actor
|
* @param actor Actor
|
||||||
* @param activity Activity to deliver
|
* @param activity Activity to deliver
|
||||||
*/
|
*/
|
||||||
@@ -115,15 +109,9 @@ class DeliverManager {
|
|||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
|
||||||
actor: { id: User['id']; host: null; },
|
actor: { id: User['id']; host: null; },
|
||||||
activity: IActivity | null,
|
activity: any,
|
||||||
) {
|
) {
|
||||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
this.actor = actor;
|
||||||
if (actor.host != null) throw new Error('actor.host must be null');
|
|
||||||
|
|
||||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
|
||||||
this.actor = {
|
|
||||||
id: actor.id,
|
|
||||||
};
|
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +155,9 @@ class DeliverManager {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async execute() {
|
public async execute() {
|
||||||
|
if (!this.userEntityService.isLocalUser(this.actor)) return;
|
||||||
|
|
||||||
// The value flags whether it is shared or not.
|
// The value flags whether it is shared or not.
|
||||||
// key: inbox URL, value: whether it is sharedInbox
|
|
||||||
const inboxes = new Map<string, boolean>();
|
const inboxes = new Map<string, boolean>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -212,6 +201,9 @@ class DeliverManager {
|
|||||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
for (const inbox of inboxes) {
|
||||||
|
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
|
||||||
|
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ export class ApMfmService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
||||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
||||||
|
|
||||||
return this.mfmService.fromHtml(html, hashtagNames);
|
return this.mfmService.fromHtml(html, hashtagNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,5 +29,5 @@ export class ApMfmService {
|
|||||||
public getNoteHtml(note: Note) {
|
public getNoteHtml(note: Note) {
|
||||||
if (!note.text) return '';
|
if (!note.text) return '';
|
||||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,9 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
|||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import type { IObject } from '../type.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApImageService {
|
export class ApImageService {
|
||||||
@@ -33,27 +32,23 @@ export class ApImageService {
|
|||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imageを作成します。
|
* Imageを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
throw new Error('actor has been suspended');
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await this.apResolverService.createResolver().resolve(value);
|
const image = await this.apResolverService.createResolver().resolve(value) as any;
|
||||||
|
|
||||||
if (image.url == null) {
|
if (image.url == null) {
|
||||||
throw new Error('invalid image: url not privided');
|
throw new Error('invalid image: url not privided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof image.url !== 'string') {
|
|
||||||
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkHttps(image.url)) {
|
if (!checkHttps(image.url)) {
|
||||||
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
||||||
}
|
}
|
||||||
@@ -62,19 +57,29 @@ export class ApImageService {
|
|||||||
|
|
||||||
const instance = await this.metaService.fetch();
|
const instance = await this.metaService.fetch();
|
||||||
|
|
||||||
const file = await this.driveService.uploadFromUrl({
|
let file = await this.driveService.uploadFromUrl({
|
||||||
url: image.url,
|
url: image.url,
|
||||||
user: actor,
|
user: actor,
|
||||||
uri: image.url,
|
uri: image.url,
|
||||||
sensitive: image.sensitive,
|
sensitive: image.sensitive,
|
||||||
isLink: !instance.cacheRemoteFiles,
|
isLink: !instance.cacheRemoteFiles,
|
||||||
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
|
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||||
});
|
});
|
||||||
if (!file.isLink || file.url === image.url) return file;
|
|
||||||
|
|
||||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する
|
if (file.isLink) {
|
||||||
await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
|
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||||
return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
|
// URLを更新する
|
||||||
|
if (file.url !== image.url) {
|
||||||
|
await this.driveFilesRepository.update({ id: file.id }, {
|
||||||
|
url: image.url,
|
||||||
|
uri: image.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +89,7 @@ export class ApImageService {
|
|||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
|
@@ -22,17 +22,17 @@ export class ApMentionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
|
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
|
||||||
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
|
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
|
||||||
|
|
||||||
const limit = promiseLimit<User | null>(2);
|
const limit = promiseLimit<User | null>(2);
|
||||||
const mentionedUsers = (await Promise.all(
|
const mentionedUsers = (await Promise.all(
|
||||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||||
)).filter((x): x is User => x != null);
|
)).filter((x): x is User => x != null);
|
||||||
|
|
||||||
return mentionedUsers;
|
return mentionedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
|
public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
|
||||||
if (tags == null) return [];
|
if (tags == null) return [];
|
||||||
|
@@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
import { ApDbResolverService } from '../ApDbResolverService.js';
|
import { ApDbResolverService } from '../ApDbResolverService.js';
|
||||||
@@ -54,7 +55,7 @@ export class ApNoteService {
|
|||||||
// 循環参照のため / for circular dependency
|
// 循環参照のため / for circular dependency
|
||||||
@Inject(forwardRef(() => ApPersonService))
|
@Inject(forwardRef(() => ApPersonService))
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private apAudienceService: ApAudienceService,
|
private apAudienceService: ApAudienceService,
|
||||||
private apMentionService: ApMentionService,
|
private apMentionService: ApMentionService,
|
||||||
@@ -71,13 +72,17 @@ export class ApNoteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string): Error | null {
|
public validateNote(object: IObject, uri: string) {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
|
|
||||||
|
if (object == null) {
|
||||||
|
return new Error('invalid Note: object is null');
|
||||||
|
}
|
||||||
|
|
||||||
if (!validPost.includes(getApType(object))) {
|
if (!validPost.includes(getApType(object))) {
|
||||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||||
}
|
}
|
||||||
@@ -86,10 +91,10 @@ export class ApNoteService {
|
|||||||
if (object.attributedTo && actualHost !== expectHost) {
|
if (object.attributedTo && actualHost !== expectHost) {
|
||||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteをフェッチします。
|
* Noteをフェッチします。
|
||||||
*
|
*
|
||||||
@@ -99,30 +104,31 @@ export class ApNoteService {
|
|||||||
public async fetchNote(object: string | IObject): Promise<Note | null> {
|
public async fetchNote(object: string | IObject): Promise<Note | null> {
|
||||||
return await this.apDbResolverService.getNoteFromApId(object);
|
return await this.apDbResolverService.getNoteFromApId(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
|
||||||
const entryUri = getApId(value);
|
const entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri);
|
const err = this.validateNote(object, entryUri);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(`${err.message}`, {
|
||||||
resolver: { history: resolver.getHistory() },
|
resolver: {
|
||||||
value,
|
history: resolver.getHistory(),
|
||||||
object,
|
},
|
||||||
|
value: value,
|
||||||
|
object: object,
|
||||||
});
|
});
|
||||||
throw new Error('invalid note');
|
throw new Error('invalid note');
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = object as IPost;
|
const note = object as IPost;
|
||||||
|
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
||||||
if (note.id && !checkHttps(note.id)) {
|
if (note.id && !checkHttps(note.id)) {
|
||||||
@@ -134,25 +140,21 @@ export class ApNoteService {
|
|||||||
if (url && !checkHttps(url)) {
|
if (url && !checkHttps(url)) {
|
||||||
throw new Error('unexpected shcema of note url: ' + url);
|
throw new Error('unexpected shcema of note url: ' + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
// 投稿者をフェッチ
|
||||||
if (note.attributedTo == null) {
|
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
|
||||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
|
|
||||||
|
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
throw new Error('actor has been suspended');
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||||
let visibility = noteAudience.visibility;
|
let visibility = noteAudience.visibility;
|
||||||
const visibleUsers = noteAudience.visibleUsers;
|
const visibleUsers = noteAudience.visibleUsers;
|
||||||
|
|
||||||
// Audience (to, cc) が指定されてなかった場合
|
// Audience (to, cc) が指定されてなかった場合
|
||||||
if (visibility === 'specified' && visibleUsers.length === 0) {
|
if (visibility === 'specified' && visibleUsers.length === 0) {
|
||||||
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
||||||
@@ -160,71 +162,81 @@ export class ApNoteService {
|
|||||||
visibility = 'public';
|
visibility = 'public';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||||
const apHashtags = extractApHashtags(note.tag);
|
const apHashtags = await extractApHashtags(note.tag);
|
||||||
|
|
||||||
// 添付ファイル
|
// 添付ファイル
|
||||||
// TODO: attachmentは必ずしもImageではない
|
// TODO: attachmentは必ずしもImageではない
|
||||||
// TODO: attachmentは必ずしも配列ではない
|
// TODO: attachmentは必ずしも配列ではない
|
||||||
const limit = promiseLimit<DriveFile>(2);
|
// Noteがsensitiveなら添付もsensitiveにする
|
||||||
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
const limit = promiseLimit(2);
|
||||||
limit(() => this.apImageService.resolveImage(actor, {
|
|
||||||
...attach,
|
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
|
||||||
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
const files = note.attachment
|
||||||
}))
|
.map(attach => attach.sensitive = note.sensitive)
|
||||||
))));
|
? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
|
||||||
|
.filter(image => image != null)
|
||||||
|
: [];
|
||||||
|
|
||||||
// リプライ
|
// リプライ
|
||||||
const reply: Note | null = note.inReplyTo
|
const reply: Note | null = note.inReplyTo
|
||||||
? await this.resolveNote(note.inReplyTo, resolver)
|
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
|
||||||
.then(x => {
|
if (x == null) {
|
||||||
if (x == null) {
|
this.logger.warn('Specified inReplyTo, but not found');
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
throw new Error('inReplyTo not found');
|
||||||
throw new Error('inReplyTo not found');
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
})
|
}
|
||||||
.catch(async err => {
|
}).catch(async err => {
|
||||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
let quote: Note | undefined | null = null;
|
let quote: Note | undefined | null;
|
||||||
|
|
||||||
if (note._misskey_quote || note.quoteUrl) {
|
if (note._misskey_quote || note.quoteUrl) {
|
||||||
const tryResolveNote = async (uri: string): Promise<
|
const tryResolveNote = async (uri: string): Promise<{
|
||||||
| { status: 'ok'; res: Note }
|
status: 'ok';
|
||||||
| { status: 'permerror' | 'temperror' }
|
res: Note | null;
|
||||||
> => {
|
} | {
|
||||||
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
status: 'permerror' | 'temperror';
|
||||||
|
}> => {
|
||||||
|
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri);
|
const res = await this.resolveNote(uri);
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res) {
|
||||||
return { status: 'ok', res };
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
res,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: 'permerror',
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
||||||
const results = await Promise.all(uris.map(tryResolveNote));
|
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
||||||
|
|
||||||
quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw new Error('quote resolve failed');
|
throw new Error('quote resolve failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cw = note.summary === '' ? null : note.summary;
|
const cw = note.summary === '' ? null : note.summary;
|
||||||
|
|
||||||
// テキストのパース
|
// テキストのパース
|
||||||
let text: string | null = null;
|
let text: string | null = null;
|
||||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
||||||
@@ -234,38 +246,38 @@ export class ApNoteService {
|
|||||||
} else if (typeof note.content === 'string') {
|
} else if (typeof note.content === 'string') {
|
||||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
if (reply && reply.hasPoll) {
|
||||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
||||||
|
|
||||||
const tryCreateVote = async (name: string, index: number): Promise<null> => {
|
const tryCreateVote = async (name: string, index: number): Promise<null> => {
|
||||||
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
|
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
|
||||||
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||||
} else if (index >= 0) {
|
} else if (index >= 0) {
|
||||||
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||||
await this.pollService.vote(actor, reply, index);
|
await this.pollService.vote(actor, reply, index);
|
||||||
|
|
||||||
// リモートフォロワーにUpdate配信
|
// リモートフォロワーにUpdate配信
|
||||||
this.pollService.deliverQuestionUpdate(reply.id);
|
this.pollService.deliverQuestionUpdate(reply.id);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (note.name) {
|
if (note.name) {
|
||||||
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
|
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
||||||
this.logger.info(`extractEmojis: ${e}`);
|
this.logger.info(`extractEmojis: ${e}`);
|
||||||
return [];
|
return [] as Emoji[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const apEmojis = emojis.map(emoji => emoji.name);
|
const apEmojis = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||||
|
|
||||||
return await this.noteCreateService.create(actor, {
|
return await this.noteCreateService.create(actor, {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
files,
|
files,
|
||||||
@@ -285,7 +297,7 @@ export class ApNoteService {
|
|||||||
url: url,
|
url: url,
|
||||||
}, silent);
|
}, silent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteを解決します。
|
* Noteを解決します。
|
||||||
*
|
*
|
||||||
@@ -296,25 +308,26 @@ export class ApNoteService {
|
|||||||
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
|
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
if (uri == null) throw new Error('missing uri');
|
if (uri == null) throw new Error('missing uri');
|
||||||
|
|
||||||
// ブロックしていたら中断
|
// ブロックしてたら中断
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
|
||||||
throw new StatusError('blocked host', 451);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.fetchNote(uri);
|
const exist = await this.fetchNote(uri);
|
||||||
if (exist) return exist;
|
|
||||||
|
if (exist) {
|
||||||
|
return exist;
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
if (uri.startsWith(this.config.url)) {
|
||||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
@@ -323,61 +336,63 @@ export class ApNoteService {
|
|||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
|
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
|
if (!tags) return [];
|
||||||
|
|
||||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
const eomjiTags = toArray(tags).filter(isEmoji);
|
||||||
|
|
||||||
const existingEmojis = await this.emojisRepository.findBy({
|
const existingEmojis = await this.emojisRepository.findBy({
|
||||||
host,
|
host,
|
||||||
name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))),
|
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(eomjiTags.map(async tag => {
|
return await Promise.all(eomjiTags.map(async tag => {
|
||||||
const name = tag.name.replaceAll(':', '');
|
const name = tag.name!.replaceAll(':', '');
|
||||||
tag.icon = toSingle(tag.icon);
|
tag.icon = toSingle(tag.icon);
|
||||||
|
|
||||||
const exists = existingEmojis.find(x => x.name === name);
|
const exists = existingEmojis.find(x => x.name === name);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
if ((exists.updatedAt == null)
|
if ((tag.updated != null && exists.updatedAt == null)
|
||||||
|| (tag.id != null && exists.uri == null)
|
|| (tag.id != null && exists.uri == null)
|
||||||
|| (new Date(tag.updated) > exists.updatedAt)
|
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|
||||||
|| (tag.icon.url !== exists.originalUrl)
|
|| (tag.icon!.url !== exists.originalUrl)
|
||||||
) {
|
) {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
}, {
|
}, {
|
||||||
uri: tag.id,
|
uri: tag.id,
|
||||||
originalUrl: tag.icon.url,
|
originalUrl: tag.icon!.url,
|
||||||
publicUrl: tag.icon.url,
|
publicUrl: tag.icon!.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({ host, name });
|
return await this.emojisRepository.findOneBy({
|
||||||
if (emoji == null) throw new Error('emoji update failed');
|
host,
|
||||||
return emoji;
|
name,
|
||||||
|
}) as Emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`register emoji host=${host}, name=${name}`);
|
this.logger.info(`register emoji host=${host}, name=${name}`);
|
||||||
|
|
||||||
return await this.emojisRepository.insert({
|
return await this.emojisRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
uri: tag.id,
|
uri: tag.id,
|
||||||
originalUrl: tag.icon.url,
|
originalUrl: tag.icon!.url,
|
||||||
publicUrl: tag.icon.url,
|
publicUrl: tag.icon!.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
} as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import { User } from '@/models/entities/User.js';
|
import { User } from '@/models/entities/User.js';
|
||||||
@@ -15,6 +15,7 @@ import type Logger from '@/logger.js';
|
|||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import type { IdService } from '@/core/IdService.js';
|
import type { IdService } from '@/core/IdService.js';
|
||||||
import type { MfmService } from '@/core/MfmService.js';
|
import type { MfmService } from '@/core/MfmService.js';
|
||||||
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import { toArray } from '@/misc/prelude/array.js';
|
import { toArray } from '@/misc/prelude/array.js';
|
||||||
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
@@ -47,8 +48,6 @@ import type { IActor, IObject } from '../type.js';
|
|||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
|
|
||||||
type Field = Record<'name' | 'value', string>;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApPersonService implements OnModuleInit {
|
export class ApPersonService implements OnModuleInit {
|
||||||
private utilityService: UtilityService;
|
private utilityService: UtilityService;
|
||||||
@@ -95,10 +94,28 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
//private utilityService: UtilityService,
|
||||||
|
//private userEntityService: UserEntityService,
|
||||||
|
//private idService: IdService,
|
||||||
|
//private globalEventService: GlobalEventService,
|
||||||
|
//private metaService: MetaService,
|
||||||
|
//private federatedInstanceService: FederatedInstanceService,
|
||||||
|
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||||
|
//private cacheService: CacheService,
|
||||||
|
//private apResolverService: ApResolverService,
|
||||||
|
//private apNoteService: ApNoteService,
|
||||||
|
//private apImageService: ApImageService,
|
||||||
|
//private apMfmService: ApMfmService,
|
||||||
|
//private mfmService: MfmService,
|
||||||
|
//private hashtagService: HashtagService,
|
||||||
|
//private usersChart: UsersChart,
|
||||||
|
//private instanceChart: InstanceChart,
|
||||||
|
//private apLoggerService: ApLoggerService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit(): void {
|
onModuleInit() {
|
||||||
this.utilityService = this.moduleRef.get('UtilityService');
|
this.utilityService = this.moduleRef.get('UtilityService');
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||||
@@ -136,6 +153,10 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
private validateActor(x: IObject, uri: string): IActor {
|
private validateActor(x: IObject, uri: string): IActor {
|
||||||
const expectHost = this.punyHost(uri);
|
const expectHost = this.punyHost(uri);
|
||||||
|
|
||||||
|
if (x == null) {
|
||||||
|
throw new Error('invalid Actor: object is null');
|
||||||
|
}
|
||||||
|
|
||||||
if (!isActor(x)) {
|
if (!isActor(x)) {
|
||||||
throw new Error(`invalid Actor type '${x.type}'`);
|
throw new Error(`invalid Actor type '${x.type}'`);
|
||||||
}
|
}
|
||||||
@@ -197,19 +218,21 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
||||||
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined;
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
|
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null;
|
const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
|
||||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
|
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.cacheService.uriPersonCache.set(uri, exist);
|
this.cacheService.uriPersonCache.set(uri, exist);
|
||||||
@@ -231,11 +254,9 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(uri);
|
const object = await resolver.resolve(uri) as any;
|
||||||
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
|
||||||
|
|
||||||
const person = this.validateActor(object, uri);
|
const person = this.validateActor(object, uri);
|
||||||
|
|
||||||
@@ -243,9 +264,9 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
const host = this.punyHost(object.id);
|
const host = this.punyHost(object.id);
|
||||||
|
|
||||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
const { fields } = this.analyzeAttachments(person.attachment ?? []);
|
||||||
|
|
||||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||||
|
|
||||||
const isBot = getApType(object) === 'Service';
|
const isBot = getApType(object) === 'Service';
|
||||||
|
|
||||||
@@ -258,7 +279,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
let user: RemoteUser | null = null;
|
let user: RemoteUser;
|
||||||
try {
|
try {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
@@ -269,16 +290,16 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
isLocked: person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo,
|
movedToUri: person.movedTo,
|
||||||
movedAt: person.movedTo ? new Date() : null,
|
movedAt: person.movedTo ? new Date() : null,
|
||||||
alsoKnownAs: person.alsoKnownAs,
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
usernameLower: person.preferredUsername?.toLowerCase(),
|
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||||
host,
|
host,
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured ? getApId(person.featured) : undefined,
|
featured: person.featured ? getApId(person.featured) : undefined,
|
||||||
uri: person.id,
|
uri: person.id,
|
||||||
@@ -290,9 +311,9 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||||
url,
|
url: url,
|
||||||
fields,
|
fields,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday ? bday[0] : null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
userHost: host,
|
userHost: host,
|
||||||
}));
|
}));
|
||||||
@@ -309,18 +330,21 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
// duplicate key error
|
// duplicate key error
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||||
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
const u = await this.usersRepository.findOneBy({
|
||||||
if (u == null) throw new Error('already registered');
|
uri: person.id,
|
||||||
|
});
|
||||||
|
|
||||||
user = u as RemoteUser;
|
if (u) {
|
||||||
|
user = u as RemoteUser;
|
||||||
|
} else {
|
||||||
|
throw new Error('already registered');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
this.federatedInstanceService.fetch(host).then(async i => {
|
this.federatedInstanceService.fetch(host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||||
@@ -330,26 +354,29 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.usersChart.update(user, true);
|
this.usersChart.update(user!, true);
|
||||||
|
|
||||||
// ハッシュタグ更新
|
// ハッシュタグ更新
|
||||||
this.hashtagService.updateUsertags(user, tags);
|
this.hashtagService.updateUsertags(user!, tags);
|
||||||
|
|
||||||
//#region アバターとヘッダー画像をフェッチ
|
//#region アバターとヘッダー画像をフェッチ
|
||||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
const [avatar, banner] = await Promise.all([
|
||||||
if (img == null) return null;
|
person.icon,
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
person.image,
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
].map(img =>
|
||||||
}));
|
img == null
|
||||||
|
? Promise.resolve(null)
|
||||||
|
: this.apImageService.resolveImage(user!, img).catch(() => null),
|
||||||
|
));
|
||||||
|
|
||||||
const avatarId = avatar?.id ?? null;
|
const avatarId = avatar ? avatar.id : null;
|
||||||
const bannerId = banner?.id ?? null;
|
const bannerId = banner ? banner.id : null;
|
||||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||||
const avatarBlurhash = avatar?.blurhash ?? null;
|
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||||
const bannerBlurhash = banner?.blurhash ?? null;
|
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user!.id, {
|
||||||
avatarId,
|
avatarId,
|
||||||
bannerId,
|
bannerId,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
@@ -358,35 +385,37 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
bannerBlurhash,
|
bannerBlurhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
user.avatarId = avatarId;
|
user!.avatarId = avatarId;
|
||||||
user.bannerId = bannerId;
|
user!.bannerId = bannerId;
|
||||||
user.avatarUrl = avatarUrl;
|
user!.avatarUrl = avatarUrl;
|
||||||
user.bannerUrl = bannerUrl;
|
user!.bannerUrl = bannerUrl;
|
||||||
user.avatarBlurhash = avatarBlurhash;
|
user!.avatarBlurhash = avatarBlurhash;
|
||||||
user.bannerBlurhash = bannerBlurhash;
|
user!.bannerBlurhash = bannerBlurhash;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region カスタム絵文字取得
|
//#region カスタム絵文字取得
|
||||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||||
this.logger.info(`extractEmojis: ${err}`);
|
this.logger.info(`extractEmojis: ${err}`);
|
||||||
return [];
|
return [] as Emoji[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiNames = emojis.map(emoji => emoji.name);
|
const emojiNames = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, { emojis: emojiNames });
|
await this.usersRepository.update(user!.id, {
|
||||||
|
emojis: emojiNames,
|
||||||
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
|
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||||
|
|
||||||
return user;
|
return user!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Personの情報を更新します。
|
* Personの情報を更新します。
|
||||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||||
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。
|
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。
|
||||||
*
|
*
|
||||||
* @param uri URI of Person
|
* @param uri URI of Person
|
||||||
* @param resolver Resolver
|
* @param resolver Resolver
|
||||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
||||||
@@ -397,14 +426,18 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) return;
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||||
if (exist === null) return;
|
|
||||||
|
if (exist === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = hint ?? await resolver.resolve(uri);
|
const object = hint ?? await resolver.resolve(uri);
|
||||||
@@ -414,22 +447,26 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
this.logger.info(`Updating the Person: ${person.id}`);
|
this.logger.info(`Updating the Person: ${person.id}`);
|
||||||
|
|
||||||
// アバターとヘッダー画像をフェッチ
|
// アバターとヘッダー画像をフェッチ
|
||||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
const [avatar, banner] = await Promise.all([
|
||||||
if (img == null) return null;
|
person.icon,
|
||||||
return this.apImageService.resolveImage(exist, img).catch(() => null);
|
person.image,
|
||||||
}));
|
].map(img =>
|
||||||
|
img == null
|
||||||
|
? Promise.resolve(null)
|
||||||
|
: this.apImageService.resolveImage(exist, img).catch(() => null),
|
||||||
|
));
|
||||||
|
|
||||||
// カスタム絵文字取得
|
// カスタム絵文字取得
|
||||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
||||||
this.logger.info(`extractEmojis: ${e}`);
|
this.logger.info(`extractEmojis: ${e}`);
|
||||||
return [];
|
return [] as Emoji[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiNames = emojis.map(emoji => emoji.name);
|
const emojiNames = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
const { fields } = this.analyzeAttachments(person.attachment ?? []);
|
||||||
|
|
||||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
||||||
@@ -442,7 +479,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
const updates = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
emojis: emojiNames,
|
emojis: emojiNames,
|
||||||
@@ -450,29 +487,18 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
tags,
|
tags,
|
||||||
isBot: getApType(object) === 'Service',
|
isBot: getApType(object) === 'Service',
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
isLocked: person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
const moving = ((): boolean => {
|
const moving =
|
||||||
// 移行先がない→ある
|
// 移行先がない→ある
|
||||||
if (
|
(!exist.movedToUri && updates.movedToUri) ||
|
||||||
exist.movedToUri === null &&
|
|
||||||
updates.movedToUri
|
|
||||||
) return true;
|
|
||||||
|
|
||||||
// 移行先がある→別のもの
|
// 移行先がある→別のもの
|
||||||
if (
|
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
|
||||||
exist.movedToUri !== null &&
|
|
||||||
updates.movedToUri !== null &&
|
|
||||||
exist.movedToUri !== updates.movedToUri
|
|
||||||
) return true;
|
|
||||||
|
|
||||||
// 移行先がある→ない、ない→ないは無視
|
// 移行先がある→ない、ない→ないは無視
|
||||||
return false;
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (moving) updates.movedAt = new Date();
|
if (moving) updates.movedAt = new Date();
|
||||||
|
|
||||||
@@ -499,10 +525,10 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||||
url,
|
url: url,
|
||||||
fields,
|
fields,
|
||||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday ? bday[0] : null,
|
||||||
location: person['vcard:Address'] ?? null,
|
location: person['vcard:Address'] ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -512,10 +538,11 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
this.hashtagService.updateUsertags(exist, tags);
|
this.hashtagService.updateUsertags(exist, tags);
|
||||||
|
|
||||||
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||||
await this.followingsRepository.update(
|
await this.followingsRepository.update({
|
||||||
{ followerId: exist.id },
|
followerId: exist.id,
|
||||||
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
|
}, {
|
||||||
);
|
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
|
});
|
||||||
|
|
||||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||||
|
|
||||||
@@ -553,22 +580,27 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
||||||
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.fetchPerson(uri);
|
const exist = await this.fetchPerson(uri);
|
||||||
if (exist) return exist;
|
|
||||||
|
if (exist) {
|
||||||
|
return exist;
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
return await this.createPerson(uri, resolver);
|
return await this.createPerson(uri, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
|
public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
|
||||||
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
|
const fields: {
|
||||||
const fields: Field[] = [];
|
name: string,
|
||||||
|
value: string
|
||||||
|
}[] = [];
|
||||||
if (Array.isArray(attachments)) {
|
if (Array.isArray(attachments)) {
|
||||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||||
fields.push({
|
fields.push({
|
||||||
@@ -578,11 +610,11 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return { fields };
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> {
|
public async updateFeatured(userId: User['id'], resolver?: Resolver) {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
@@ -611,13 +643,13 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
// とりあえずidを別の時間で生成して順番を維持
|
// とりあえずidを別の時間で生成して順番を維持
|
||||||
let td = 0;
|
let td = 0;
|
||||||
for (const note of featuredNotes.filter((note): note is Note => note != null)) {
|
for (const note of featuredNotes.filter(note => note != null)) {
|
||||||
td -= 1000;
|
td -= 1000;
|
||||||
transactionalEntityManager.insert(UserNotePining, {
|
transactionalEntityManager.insert(UserNotePining, {
|
||||||
id: this.idService.genId(new Date(Date.now() + td)),
|
id: this.idService.genId(new Date(Date.now() + td)),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
noteId: note.id,
|
noteId: note!.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -656,7 +688,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
|
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
|
||||||
dst = await this.resolvePerson(src.movedToUri);
|
dst = await this.resolvePerson(src.movedToUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
||||||
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
||||||
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
||||||
|
@@ -4,12 +4,12 @@ import type { NotesRepository, PollsRepository } from '@/models/index.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { IPoll } from '@/models/entities/Poll.js';
|
import type { IPoll } from '@/models/entities/Poll.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import { isQuestion } from '../type.js';
|
import { isQuestion } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IQuestion } from '../type.js';
|
import type { IObject, IQuestion } from '../type.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApQuestionService {
|
export class ApQuestionService {
|
||||||
@@ -33,25 +33,33 @@ export class ApQuestionService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const question = await resolver.resolve(source);
|
const question = await resolver.resolve(source);
|
||||||
if (!isQuestion(question)) throw new Error('invalid type');
|
|
||||||
|
|
||||||
const multiple = question.oneOf === undefined;
|
if (!isQuestion(question)) {
|
||||||
if (multiple && question.anyOf === undefined) throw new Error('invalid question');
|
throw new Error('invalid type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiple = !question.oneOf;
|
||||||
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
|
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
|
||||||
|
|
||||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
if (multiple && !question.anyOf) {
|
||||||
?.map((x) => x.name)
|
throw new Error('invalid question');
|
||||||
.filter((x): x is string => typeof x === 'string')
|
}
|
||||||
?? [];
|
|
||||||
|
|
||||||
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
const choices = question[multiple ? 'anyOf' : 'oneOf']!
|
||||||
|
.map((x, i) => x.name!);
|
||||||
|
|
||||||
return { choices, votes, multiple, expiresAt };
|
const votes = question[multiple ? 'anyOf' : 'oneOf']!
|
||||||
|
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices,
|
||||||
|
votes,
|
||||||
|
multiple,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,9 +68,8 @@ export class ApQuestionService {
|
|||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
|
public async updateQuestion(value: any, resolver?: Resolver) {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uri = typeof value === 'string' ? value : value.id;
|
||||||
if (uri == null) throw new Error('uri is null');
|
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
||||||
@@ -76,7 +83,6 @@ export class ApQuestionService {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value) as IQuestion;
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
@@ -84,14 +90,12 @@ export class ApQuestionService {
|
|||||||
if (question.type !== 'Question') throw new Error('object is not a Question');
|
if (question.type !== 'Question') throw new Error('object is not a Question');
|
||||||
|
|
||||||
const apChoices = question.oneOf ?? question.anyOf;
|
const apChoices = question.oneOf ?? question.anyOf;
|
||||||
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const choice of poll.choices) {
|
for (const choice of poll.choices) {
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||||
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
|
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
|
||||||
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
|
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -99,7 +103,9 @@ export class ApQuestionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes });
|
await this.pollsRepository.update({ noteId: note.id }, {
|
||||||
|
votes: poll.votes,
|
||||||
|
});
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { toArray } from '@/misc/prelude/array.js';
|
|||||||
import { isHashtag } from '../type.js';
|
import { isHashtag } from '../type.js';
|
||||||
import type { IObject, IApHashtag } from '../type.js';
|
import type { IObject, IApHashtag } from '../type.js';
|
||||||
|
|
||||||
export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] {
|
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
|
||||||
if (tags == null) return [];
|
if (tags == null) return [];
|
||||||
|
|
||||||
const hashtags = extractApHashtagObjects(tags);
|
const hashtags = extractApHashtagObjects(tags);
|
||||||
|
@@ -47,7 +47,7 @@ export class DriveFileEntityService {
|
|||||||
private videoProcessingService: VideoProcessingService,
|
private videoProcessingService: VideoProcessingService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateFileName(name: string): boolean {
|
public validateFileName(name: string): boolean {
|
||||||
return (
|
return (
|
||||||
|
@@ -24,7 +24,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
private driveFileEntityService: DriveFileEntityService;
|
private driveFileEntityService: DriveFileEntityService;
|
||||||
private customEmojiService: CustomEmojiService;
|
private customEmojiService: CustomEmojiService;
|
||||||
private reactionService: ReactionService;
|
private reactionService: ReactionService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||||
this.reactionService = this.moduleRef.get('ReactionService');
|
this.reactionService = this.moduleRef.get('ReactionService');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
|
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
|
||||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||||
@@ -457,12 +457,12 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.userId = :userId', { userId })
|
.where('note.userId = :userId', { userId })
|
||||||
.andWhere('note.renoteId = :renoteId', { renoteId });
|
.andWhere('note.renoteId = :renoteId', { renoteId });
|
||||||
|
|
||||||
// 指定した投稿を除く
|
// 指定した投稿を除く
|
||||||
if (excludeNoteId) {
|
if (excludeNoteId) {
|
||||||
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
|
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query.getCount();
|
return await query.getCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
private noteEntityService: NoteEntityService;
|
private noteEntityService: NoteEntityService;
|
||||||
private reactionService: ReactionService;
|
private reactionService: ReactionService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
meId: User['id'],
|
meId: User['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
options: {
|
options: {
|
||||||
|
|
||||||
},
|
},
|
||||||
hint?: {
|
hint?: {
|
||||||
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, Not } from 'typeorm';
|
import { In, Not } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import _Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@@ -31,7 +31,6 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
|
|||||||
Packed<'UserDetailed'> :
|
Packed<'UserDetailed'> :
|
||||||
Packed<'UserLite'>;
|
Packed<'UserLite'>;
|
||||||
|
|
||||||
const Ajv = _Ajv.default;
|
|
||||||
const ajv = new Ajv();
|
const ajv = new Ajv();
|
||||||
|
|
||||||
function isLocalUser(user: User): user is LocalUser;
|
function isLocalUser(user: User): user is LocalUser;
|
||||||
@@ -113,7 +112,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
@Inject(DI.pagesRepository)
|
@Inject(DI.pagesRepository)
|
||||||
private pagesRepository: PagesRepository,
|
private pagesRepository: PagesRepository,
|
||||||
|
|
||||||
@Inject(DI.userMemosRepository)
|
@Inject(DI.userMemosRepository)
|
||||||
private userMemosRepository: UserMemoRepository,
|
private userMemosRepository: UserMemoRepository,
|
||||||
|
|
||||||
|
@@ -81,7 +81,7 @@ export class QueueStatsService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
this.intervalId = setInterval(tick, interval);
|
this.intervalId = setInterval(tick, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
|
@@ -3,7 +3,6 @@ import si from 'systeminformation';
|
|||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
import * as osUtils from 'os-utils';
|
import * as osUtils from 'os-utils';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
@@ -15,10 +14,9 @@ const round = (num: number) => Math.round(num * 10) / 10;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerStatsService implements OnApplicationShutdown {
|
export class ServerStatsService implements OnApplicationShutdown {
|
||||||
private intervalId: NodeJS.Timer | null = null;
|
private intervalId: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private metaService: MetaService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +24,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||||||
* Report server stats regularly
|
* Report server stats regularly
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async start(): Promise<void> {
|
public start(): void {
|
||||||
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
|
|
||||||
|
|
||||||
const log = [] as any[];
|
const log = [] as any[];
|
||||||
|
|
||||||
ev.on('requestServerStatsLog', x => {
|
ev.on('requestServerStatsLog', x => {
|
||||||
@@ -68,9 +64,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (this.intervalId) {
|
clearInterval(this.intervalId);
|
||||||
clearInterval(this.intervalId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -4,7 +4,7 @@ import { default as convertColor } from 'color-convert';
|
|||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { envOption } from './env.js';
|
import { envOption } from './env.js';
|
||||||
import type { KEYWORD } from 'color-convert/conversions.js';
|
import type { KEYWORD } from 'color-convert/conversions';
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@@ -83,16 +83,6 @@ export class RedisKVCache<T> {
|
|||||||
|
|
||||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public gc() {
|
|
||||||
this.memoryCache.gc();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose() {
|
|
||||||
this.memoryCache.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RedisSingleCache<T> {
|
export class RedisSingleCache<T> {
|
||||||
@@ -184,15 +174,10 @@ export class RedisSingleCache<T> {
|
|||||||
export class MemoryKVCache<T> {
|
export class MemoryKVCache<T> {
|
||||||
public cache: Map<string, { date: number; value: T; }>;
|
public cache: Map<string, { date: number; value: T; }>;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
private gcIntervalHandle: NodeJS.Timer;
|
|
||||||
|
|
||||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
|
|
||||||
this.gcIntervalHandle = setInterval(() => {
|
|
||||||
this.gc();
|
|
||||||
}, 1000 * 60 * 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -215,7 +200,7 @@ export class MemoryKVCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public delete(key: string): void {
|
public delete(key: string) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,21 +255,6 @@ export class MemoryKVCache<T> {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public gc(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
for (const [key, { date }] of this.cache.entries()) {
|
|
||||||
if ((now - date) > this.lifetime) {
|
|
||||||
this.cache.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
clearInterval(this.gcIntervalHandle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemorySingleCache<T> {
|
export class MemorySingleCache<T> {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
export function checkHttps(url: string): boolean {
|
export function checkHttps(url: string) {
|
||||||
return url.startsWith('https://') ||
|
return url.startsWith('https://') ||
|
||||||
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
|
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
|
||||||
export default () => secureRndstr(16);
|
export default () => secureRndstr(16, true);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import IPCIDR from 'ip-cidr';
|
import IPCIDR from 'ip-cidr';
|
||||||
|
|
||||||
export function getIpHash(ip: string): string {
|
export function getIpHash(ip: string) {
|
||||||
try {
|
try {
|
||||||
// because a single person may control many IPv6 addresses,
|
// because a single person may control many IPv6 addresses,
|
||||||
// only a /64 subnet prefix of any IP will be taken into account.
|
// only a /64 subnet prefix of any IP will be taken into account.
|
||||||
|
@@ -131,7 +131,7 @@ type NullOrUndefined<p extends Schema, T> =
|
|||||||
| T;
|
| T;
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||||
// Get intersection from union
|
// Get intersection from union
|
||||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||||
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
|
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* 1. 配列に何も入っていない時はクエリを付けない
|
* 1. 配列に何も入っていない時はクエリを付けない
|
||||||
* 2. プロパティがundefinedの時はクエリを付けない
|
* 2. プロパティがundefinedの時はクエリを付けない
|
||||||
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
|
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
|
||||||
*/
|
*/
|
||||||
export function query(obj: Record<string, unknown>): string {
|
export function query(obj: Record<string, unknown>): string {
|
||||||
const params = Object.entries(obj)
|
const params = Object.entries(obj)
|
||||||
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
|
|
||||||
export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
|
const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string {
|
export function secureRndstr(length = 32, useLU = true): string {
|
||||||
|
const chars = useLU ? LU_CHARS : L_CHARS;
|
||||||
const chars_len = chars.length;
|
const chars_len = chars.length;
|
||||||
|
|
||||||
let str = '';
|
let str = '';
|
||||||
|
@@ -55,10 +55,7 @@ export class Ad {
|
|||||||
length: 8192, nullable: false,
|
length: 8192, nullable: false,
|
||||||
})
|
})
|
||||||
public memo: string;
|
public memo: string;
|
||||||
@Column('integer', {
|
|
||||||
default: 0, nullable: false,
|
|
||||||
})
|
|
||||||
public dayOfWeek: number;
|
|
||||||
constructor(data: Partial<Ad>) {
|
constructor(data: Partial<Ad>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
@@ -97,30 +97,18 @@ export class Meta {
|
|||||||
})
|
})
|
||||||
public logoImageUrl: string | null;
|
public logoImageUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public errorImageUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024,
|
length: 1024,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public iconUrl: string | null;
|
public iconUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 1024,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public serverErrorImageUrl: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 1024,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public notFoundImageUrl: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 1024,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public infoImageUrl: string | null;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
@@ -413,16 +401,6 @@ export class Meta {
|
|||||||
})
|
})
|
||||||
public enableChartsForFederatedInstances: boolean;
|
public enableChartsForFederatedInstances: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public enableServerMachineStats: boolean;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: true,
|
|
||||||
})
|
|
||||||
public enableIdenticonGeneration: boolean;
|
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: { },
|
default: { },
|
||||||
})
|
})
|
||||||
|
@@ -207,7 +207,7 @@ export class UserProfile {
|
|||||||
public mutedInstances: string[];
|
public mutedInstances: string[];
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('enum', {
|
||||||
enum: [
|
enum: [
|
||||||
...notificationTypes,
|
...notificationTypes,
|
||||||
// マイグレーションで削除が困難なので古いenumは残しておく
|
// マイグレーションで削除が困難なので古いenumは残しておく
|
||||||
...obsoleteNotificationTypes,
|
...obsoleteNotificationTypes,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user