Compare commits

..

31 Commits

Author SHA1 Message Date
72b85fc09f 10.83.0 2019-02-08 17:06:07 +09:00
6c27412c9c Fix theme 2019-02-08 17:05:50 +09:00
46bddfc9c2 New Crowdin translations (#4178)
* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

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

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

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

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

* New translations ja-JP.yml (Chinese Simplified)
2019-02-08 17:01:43 +09:00
56275bcfcb Introduce per-instance chart (#4183)
* Introduce per-instance chart

* Implement chart view in client

* Handle note deleting

* More chart srcs

* Add drive stats

* Improve drive stats

* Fix bug

* Add icon
2019-02-08 16:58:57 +09:00
f35688bab8 Supress logs during test 2019-02-08 16:56:23 +09:00
93541f83c8 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-08 04:40:55 +09:00
ea0d114833 🎨 2019-02-08 04:40:47 +09:00
7f6a3ec828 🎨 2019-02-08 04:36:23 +09:00
732804b6fa Update CONTRIBUTING.md 2019-02-08 04:33:15 +09:00
aba85b977d Refactoring: Move chart dir into services dir 2019-02-08 04:31:33 +09:00
e6612f610c Implement instance blocking (#4182)
* Implement instance blocking

* Add missing text

* Delete unnecessary file

* Covert Punycode to Unicode
2019-02-08 04:26:43 +09:00
5a28632af7 Update CONTRIBUTING.md 2019-02-08 04:08:25 +09:00
4099db0d42 [Client] Add icon 🎨 2019-02-07 23:42:56 +09:00
9d50a06d9c Fix bug 2019-02-07 23:37:39 +09:00
dd7bf9b2a3 Remove unused import 2019-02-07 23:32:39 +09:00
c463284c2f Fix bug 2019-02-07 23:27:42 +09:00
c1d728a616 インスタンス一覧の表示数を増やした 2019-02-07 22:00:55 +09:00
e43c9c0e21 特定インスタンスからのフォローを全解除できるように 2019-02-07 21:59:18 +09:00
15cac10d7b 10.82.4 2019-02-07 21:30:38 +09:00
49958ca03f Make instance information more detail 2019-02-07 21:23:12 +09:00
280dbe9853 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-07 21:07:58 +09:00
bf964ee969 Update load.ts 2019-02-07 21:03:24 +09:00
61dcd51888 Revert "Fix bug"
This reverts commit 2ef795aba8.
2019-02-07 21:02:57 +09:00
5448c22031 Revert 96bc17aa10 2019-02-07 21:02:33 +09:00
27768081e2 Fix #4179 2019-02-07 20:14:15 +09:00
c3140f57b9 連合しているインスタンスを一覧できるように 2019-02-07 18:11:20 +09:00
7275bc6d3b Improve instance stats 2019-02-07 16:05:29 +09:00
485f2f460e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-07 15:00:52 +09:00
336912e442 Improve instance stats 2019-02-07 15:00:44 +09:00
dd9c94e47e Update CONTRIBUTING.md 2019-02-07 14:54:14 +09:00
055863144d Update issue templates (#4038)
* WIP: Update issue templates

* Update client-side-feature-request.md

* Update bug_report.md

* Update feature_request.md

* Update server-side-bug-report.md

* Update server-side-feature-request.md

* Update bug_report.md
2019-02-07 14:46:17 +09:00
101 changed files with 2118 additions and 360 deletions

View File

@ -1,22 +1,30 @@
---
name: Bug Report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
# Summary
<!-- Tell us what the bug is -->
# Expected Behavior
<!--- Tell us what should happen -->
# Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
# Steps to Reproduce
1.
2.
3.
# Environment
<!-- Tell us where on the platform it happens -->
<!-- e.g. desktop or mobile version, your browser, your OS -->

View File

@ -0,0 +1,31 @@
---
name: Client-side Bug Report
about: Create a report to help us improve
title: ''
labels: bug, client-side
assignees: ''
---
# Summary
<!-- Tell us what the bug is -->
# Expected Behavior
<!--- Tell us what should happen -->
# Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
# Steps to Reproduce
1.
2.
3.
# Environment
<!-- Tell us where on the platform it happens -->
<!-- e.g. desktop or mobile version, your browser, your OS -->

View File

@ -0,0 +1,12 @@
---
name: Client-side Feature Request
about: Suggest an idea for this project
title: ''
labels: client-side, feature
assignees: ''
---
# Summary
<!-- Tell us what the suggestion is -->

View File

@ -1,11 +1,12 @@
---
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
# Summary
<!-- Tell us what the suggestion is -->
# Environment
<!-- Tell us where on the platform it related -->
<!-- e.g. desktop or mobile version, your browser, your OS -->
<!-- Tell us what the suggestion is -->

View File

@ -0,0 +1,31 @@
---
name: Server-side Bug Report
about: Create a report to help us improve
title: ''
labels: bug, server-side
assignees: ''
---
# Summary
<!-- Tell us what the bug is -->
# Expected Behavior
<!--- Tell us what should happen -->
# Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
# Steps to Reproduce
1.
2.
3.
# Environment
<!-- Tell us where on the platform it happens -->
<!-- e.g. your Node.js version, your OS -->

View File

@ -0,0 +1,12 @@
---
name: Server-side Feature Request
about: Suggest an idea for this project
title: ''
labels: feature, server-side
assignees: ''
---
# Summary
<!-- Tell us what the suggestion is -->

View File

@ -1,6 +1,16 @@
ChangeLog
=========
10.83.0
----------
* 特定のインスタンスをブロックをできるように
* 特定のインスタンスからのフォローを全解除できるように
* インスタンスごとのチャートを追加
10.82.4
----------
* 起動できなくなることがある問題を修正
10.82.3
----------
* フォロー/ミュート/ブロックデータをエクスポート可能に

View File

@ -44,3 +44,31 @@ Stands for _**S**ervice**W**orker_.
#### Denyaize
Nyaizeを解除すること
## Code style
### Don't use `export default`
Bad:
``` ts
export default function(foo: string): string {
```
Good:
``` ts
export function something(foo: string): string {
```
## Directory structure
```
src ... ソースコード
@types ... 外部ライブラリなどの型定義
prelude ... Misskeyに関係ないかつ副作用なし
misc ... 副作用なしのユーティリティ処理
service ... 副作用ありの共通処理
queue ... ジョブキューとジョブ
server ... Webサーバー
client ... クライアント
mfm ... MFM
test ... テスト
```

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1019,7 +1019,7 @@ admin/views/dashboard.vue:
federated: "Federated"
admin/views/queue.vue:
operation: "Action(s)"
remove-all-jobs: "すべてのジョブをクリア"
remove-all-jobs: "Clear all queued jobs"
admin/views/abuse.vue:
title: "Abuse"
target: "Target"
@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "Deleted"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "Federation"
host: "Host"
notes: "Notes"
users: "Users"
following: "Following"
followers: "Followers"
status: "Status"
latest-request-sent-at: "Time of last request sent"
latest-request-received-at: "Last request received at"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "Block"
lookup: "Look up"
instances: "Instances"
instance-not-registered: "The instance has not been discovered"
sort: "Sort by"
sorts:
caughtAtAsc: "Date of discovery (Ascending)"
caughtAtDesc: "Date of discovery (Descending)"
notesAsc: "Order by least Notes posted"
notesDesc: "Order by most Notes posted"
usersAsc: "Less followers"
usersDesc: "More followers"
followingAsc: "Least followed"
followingDesc: "Has more followers"
followersAsc: "Sort by having less followers"
followersDesc: "Sort by the larger number of followers"
state: "Status"
states:
all: "All"
blocked: "Blocked"
result-is-truncated: "Displaying the top {n} items."
desktop/views/pages/welcome.vue:
about: "More details..."
gotit: "Got it!"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -345,8 +345,8 @@ common/views/components/note-menu.vue:
copy-link: "Copier le lien"
favorite: "Mettre cette note en favoris"
unfavorite: "Retirer des favoris"
watch: "ウォッチ"
unwatch: "ウォッチ解除"
watch: "Surveiller"
unwatch: "Ne plus surveiller"
pin: "Épingler sur votre profil"
unpin: "Désépingler"
delete: "Supprimer"
@ -363,10 +363,10 @@ common/views/components/user-menu.vue:
report-abuse: "Signaler un abus"
report-abuse-detail: "Détail du signalement"
report-abuse-reported: "Transmit à ladministrateur. Merci de votre collaboration."
silence: "サイレンス"
unsilence: "サイレンス解除"
silence: "Mettre en sourdine"
unsilence: "Enlever la sourdine"
suspend: "Suspendre"
unsuspend: "凍結解除"
unsuspend: "Ne plus suspendre"
common/views/components/poll.vue:
vote-to: "Voter pour '{}'"
vote-count: "{} votes"
@ -509,12 +509,12 @@ common/views/components/profile-editor.vue:
email-address: "Adresse de courrier électronique"
email-verified: "Ladresse du courrier électronique a été vérifiée."
email-not-verified: "Adresse de courriel nest pas confirmée. Veuillez vérifier votre boite de réception."
export: "エクスポート"
export: "Exporter"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
all-notes: "Toutes les notes publiées"
following-list: "Liste des abonnements"
mute-list: "Liste des comptes mis en sourdine"
blocking-list: "Liste des comptes bloqués"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue:
users: "Utilisateur·rice"
@ -1007,7 +1007,7 @@ admin/views/index.vue:
announcements: "Annonces"
hashtags: "Hashtags"
abuse: "Abus"
queue: "ジョブキュー"
queue: "File dattente"
back-to-misskey: "Retour vers Misskey"
admin/views/dashboard.vue:
dashboard: "Tableau de bord"
@ -1018,7 +1018,7 @@ admin/views/dashboard.vue:
this-instance: "Cette instance"
federated: "Fédérées"
admin/views/queue.vue:
operation: "操作"
operation: "Action(s)"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue:
title: "Abus"
@ -1161,8 +1161,8 @@ admin/views/users.vue:
unsuspend: "Suspension levée"
unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?"
unsuspended: "La suspension de lutilisateur a été levée avec succès"
make-silence: "サイレンス"
unmake-silence: "サイレンスの解除"
make-silence: "Mettre en sourdine"
unmake-silence: "Enlever la sourdine"
verify: "Vérification du compte"
verify-confirm: "Souhaiteriez-vous rendre votre compte comme étant un compte vérifié ?"
verified: "Le compte a été vérifié"
@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "Supprimé"
admin/views/hashtags.vue:
hided-tags: "Tags cachés"
admin/views/federation.vue:
federation: "Fédération"
host: "Hôte"
notes: "Notes"
users: "Utilisateur·rice·s"
following: "Abonnements"
followers: "Abonné·e·s"
status: "Statuts"
latest-request-sent-at: "Dernière requête envoyée"
latest-request-received-at: "Dernière requête reçue"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "Instances"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "Trier par"
sorts:
caughtAtAsc: "Date dinscription (Ascendant)"
caughtAtDesc: "Date dinscription (Descendant)"
notesAsc: "投稿が少ない順"
notesDesc: "Description des notes"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "Les moins suivies"
followingDesc: "フォローが多い順"
followersAsc: "Ayant le moins d'abonné·e·s"
followersDesc: "Ayant le plus d'abonné·e·s"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "à propos"
gotit: "J'ai compris !"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1371,6 +1371,60 @@ admin/views/announcements.vue:
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
driveUsageAsc: "ドライブ使用量が少ない順"
driveUsageDesc: "ドライブ使用量が多い順"
driveFilesAsc: "ドライブのファイル数が少ない順"
driveFilesDesc: "ドライブのファイル数が多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
charts: "チャート"
chart-srcs:
requests: "リクエスト"
users: "ユーザーの増減"
users-total: "ユーザーの積算"
notes: "投稿の増減"
notes-total: "投稿の積算"
ff: "フォロー/フォロワーの増減"
ff-total: "フォロー/フォロワーの積算"
drive-usage: "ドライブ使用量の増減"
drive-usage-total: "ドライブ使用量の増減"
drive-files: "ドライブファイル数の増減"
drive-files-total: "ドライブファイル数の増減"
chart-spans:
hour: "1時間ごと"
day: "1日ごと"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "もうちょい……"
gotit: "ほい"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "삭제하였습니다"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "자세히..."
gotit: "알겠습니다"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "Skjønner!"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "Usunięto"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "O Misskey"
gotit: "Rozumiem!"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue:
federation: "連合"
host: "ホスト"
notes: "投稿"
users: "ユーザー"
following: "フォロー中"
followers: "フォロワー"
status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック"
lookup: "照会"
instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
state: "状態"
states:
all: "すべて"
blocked: "ブロック"
result-is-truncated: "上位{n}件を表示しています。"
desktop/views/pages/welcome.vue:
about: "詳しく..."
gotit: "わかった"

View File

@ -354,10 +354,10 @@ common/views/components/note-menu.vue:
remote: "显示原始投稿"
common/views/components/user-menu.vue:
mention: "提到"
mute: "免打扰"
unmute: "解除免打扰"
block: "屏蔽"
unblock: "取消屏蔽"
mute: "屏蔽"
unmute: "解除屏蔽"
block: "拉黑"
unblock: "取消拉黑"
push-to-list: "添加至列表"
select-list: "请选择一个列表"
report-abuse: "举报骚扰"
@ -511,10 +511,10 @@ common/views/components/profile-editor.vue:
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
export: "导出"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
all-notes: "所有发帖"
following-list: "关注列表"
mute-list: "屏蔽列表"
blocking-list: "黑名单"
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
common/views/components/user-list-editor.vue:
users: "用户"
@ -780,8 +780,8 @@ desktop/views/components/settings.vue:
notification: "通知"
apps: "应用程序"
tags: "标签"
mute-and-block: "静音/屏蔽"
blocking: "屏蔽中"
mute-and-block: "屏蔽/拉黑"
blocking: "已拉黑"
security: "安全性"
signin: "登录历史"
password: "密码"
@ -912,13 +912,13 @@ common/views/components/drive-settings.vue:
in-use: "正在使用"
stats: "统计"
common/views/components/mute-and-block.vue:
mute-and-block: "静音/封锁"
mute: "静音"
block: "封锁中"
no-muted-users: "没有静音的用户"
no-blocked-users: "没有封锁的用户"
word-mute: "文字静音"
muted-words: "静音的关键字"
mute-and-block: "屏蔽/拉黑"
mute: "屏蔽"
block: "拉黑中"
no-muted-users: "无屏蔽用户"
no-blocked-users: "无拉黑的用户"
word-mute: "文字屏蔽"
muted-words: "屏蔽关键字"
muted-words-description: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范"
save: "保存"
common/views/components/password-settings.vue:
@ -1007,7 +1007,7 @@ admin/views/index.vue:
announcements: "公告"
hashtags: "标签"
abuse: "举报垃圾信息"
queue: "ジョブキュー"
queue: "作业队列"
back-to-misskey: "返回 Misskey"
admin/views/dashboard.vue:
dashboard: "Dashboard"
@ -1019,7 +1019,7 @@ admin/views/dashboard.vue:
federated: "联合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
remove-all-jobs: "清除所有作业"
admin/views/abuse.vue:
title: "举报垃圾信息"
target: "目标"
@ -1233,6 +1233,39 @@ admin/views/announcements.vue:
removed: "已删除"
admin/views/hashtags.vue:
hided-tags: "隐藏标签"
admin/views/federation.vue:
federation: "联合"
host: "主机名"
notes: "帖子"
users: "用户"
following: "正在关注"
followers: "关注者"
status: "状态"
latest-request-sent-at: "上次发送的请求"
latest-request-received-at: "上次收到的请求"
remove-all-following: "取消所有关注"
remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。"
block: "拉黑"
lookup: "查询"
instances: "实例"
instance-not-registered: "实例未注册"
sort: "排序"
sorts:
caughtAtAsc: "注册时间从旧到新"
caughtAtDesc: "注册时间从新到旧"
notesAsc: "发帖数量从少到多"
notesDesc: "发帖数量从多到少"
usersAsc: "用户数从少到多"
usersDesc: "用户数从多到少"
followingAsc: "关注数从少到多"
followingDesc: "关注数从多到少"
followersAsc: "粉丝数从少到多"
followersDesc: "粉丝数从多到少"
state: "状态"
states:
all: "所有"
blocked: "已拉黑"
result-is-truncated: "显示最前面的{n}项。"
desktop/views/pages/welcome.vue:
about: "更多信息..."
gotit: "没问题! "

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.82.3",
"clientVersion": "2.0.14180",
"version": "10.83.0",
"clientVersion": "2.0.14211",
"codename": "nighthike",
"repository": {
"type": "git",

View File

@ -124,7 +124,7 @@ export default Vue.extend({
this.meta = meta;
});
this.$root.api('instances', {
this.$root.api('federation/instances', {
sort: '+notes'
}).then(instances => {
for (const i of instances) {

View File

@ -0,0 +1,477 @@
<template>
<div>
<ui-card>
<div slot="title"><fa :icon="faTerminal"/> {{ $t('federation') }}</div>
<section class="fit-top">
<ui-input class="target" v-model="target" type="text" @enter="showInstance">
<span>{{ $t('host') }}</span>
</ui-input>
<ui-button @click="showInstance"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<div class="instance" v-if="instance">
<ui-input :value="instance.host" type="text" readonly>
<span>{{ $t('host') }}</span>
</ui-input>
<ui-horizon-group inputs>
<ui-input :value="instance.notesCount | number" type="text" readonly>
<span>{{ $t('notes') }}</span>
</ui-input>
<ui-input :value="instance.usersCount | number" type="text" readonly>
<span>{{ $t('users') }}</span>
</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input :value="instance.followingCount | number" type="text" readonly>
<span>{{ $t('following') }}</span>
</ui-input>
<ui-input :value="instance.followersCount | number" type="text" readonly>
<span>{{ $t('followers') }}</span>
</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input :value="instance.latestRequestSentAt" type="text" readonly>
<span>{{ $t('latest-request-sent-at') }}</span>
</ui-input>
<ui-input :value="instance.latestStatus" type="text" readonly>
<span>{{ $t('status') }}</span>
</ui-input>
</ui-horizon-group>
<ui-input :value="instance.latestRequestReceivedAt" type="text" readonly>
<span>{{ $t('latest-request-received-at') }}</span>
</ui-input>
<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
<details>
<summary>{{ $t('charts') }}</summary>
<ui-horizon-group inputs>
<ui-select v-model="chartSrc">
<option value="requests">{{ $t('chart-srcs.requests') }}</option>
<option value="users">{{ $t('chart-srcs.users') }}</option>
<option value="users-total">{{ $t('chart-srcs.users-total') }}</option>
<option value="notes">{{ $t('chart-srcs.notes') }}</option>
<option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option>
<option value="ff">{{ $t('chart-srcs.ff') }}</option>
<option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option>
<option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option>
<option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option>
<option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option>
<option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option>
</ui-select>
<ui-select v-model="chartSpan">
<option value="hour">{{ $t('chart-spans.hour') }}</option>
<option value="day">{{ $t('chart-spans.day') }}</option>
</ui-select>
</ui-horizon-group>
<div ref="chart"></div>
</details>
<details>
<summary>{{ $t('remove-all-following') }}</summary>
<ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button>
<ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info>
</details>
</div>
</section>
</ui-card>
<ui-card>
<div slot="title"><fa :icon="faServer"/> {{ $t('instances') }}</div>
<section class="fit-top">
<ui-horizon-group inputs>
<ui-select v-model="sort">
<span slot="label">{{ $t('sort') }}</span>
<option value="-caughtAt">{{ $t('sorts.caughtAtAsc') }}</option>
<option value="+caughtAt">{{ $t('sorts.caughtAtDesc') }}</option>
<option value="-notes">{{ $t('sorts.notesAsc') }}</option>
<option value="+notes">{{ $t('sorts.notesDesc') }}</option>
<option value="-users">{{ $t('sorts.usersAsc') }}</option>
<option value="+users">{{ $t('sorts.usersDesc') }}</option>
<option value="-following">{{ $t('sorts.followingAsc') }}</option>
<option value="+following">{{ $t('sorts.followingDesc') }}</option>
<option value="-followers">{{ $t('sorts.followersAsc') }}</option>
<option value="+followers">{{ $t('sorts.followersDesc') }}</option>
<option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option>
<option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option>
<option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option>
<option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option>
</ui-select>
<ui-select v-model="state">
<span slot="label">{{ $t('state') }}</span>
<option value="all">{{ $t('states.all') }}</option>
<option value="blocked">{{ $t('states.blocked') }}</option>
</ui-select>
</ui-horizon-group>
<div class="instances">
<header>
<span>{{ $t('host') }}</span>
<span>{{ $t('notes') }}</span>
<span>{{ $t('users') }}</span>
<span>{{ $t('following') }}</span>
<span>{{ $t('followers') }}</span>
<span>{{ $t('status') }}</span>
</header>
<div v-for="instance in instances">
<span>{{ instance.host }}</span>
<span>{{ instance.notesCount | number }}</span>
<span>{{ instance.usersCount | number }}</span>
<span>{{ instance.followingCount | number }}</span>
<span>{{ instance.followersCount | number }}</span>
<span>{{ instance.latestStatus }}</span>
</div>
</div>
<ui-info v-if="instances.length == limit">{{ $t('result-is-truncated', { n: limit }) }}</ui-info>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons';
import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2';
const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x);
export default Vue.extend({
i18n: i18n('admin/views/federation.vue'),
data() {
return {
instance: null,
target: null,
sort: '+caughtAt',
state: 'all',
limit: 50,
instances: [],
chart: null,
chartSrc: 'requests',
chartSpan: 'hour',
chartInstance: null,
faGlobe, faTerminal, faSearch, faMinusCircle, faServer
};
},
computed: {
data(): any {
if (this.chart == null) return null;
switch (this.chartSrc) {
case 'requests': return this.requestsChart();
case 'users': return this.usersChart(false);
case 'users-total': return this.usersChart(true);
case 'notes': return this.notesChart(false);
case 'notes-total': return this.notesChart(true);
case 'ff': return this.ffChart(false);
case 'ff-total': return this.ffChart(true);
case 'drive-usage': return this.driveUsageChart(false);
case 'drive-usage-total': return this.driveUsageChart(true);
case 'drive-files': return this.driveFilesChart(false);
case 'drive-files-total': return this.driveFilesChart(true);
}
},
stats(): any[] {
const stats =
this.chartSpan == 'day' ? this.chart.perDay :
this.chartSpan == 'hour' ? this.chart.perHour :
null;
return stats;
}
},
watch: {
sort() {
this.fetchInstances();
},
state() {
this.fetchInstances();
},
async instance() {
this.now = new Date();
const [perHour, perDay] = await Promise.all([
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
]);
const chart = {
perHour: perHour,
perDay: perDay
};
this.chart = chart;
this.renderChart();
},
chartSrc() {
this.renderChart();
},
chartSpan() {
this.renderChart();
}
},
mounted() {
this.fetchInstances();
},
beforeDestroy() {
this.chartInstance.destroy();
},
methods: {
showInstance() {
this.$root.api('federation/show-instance', {
host: this.target
}).then(instance => {
if (instance == null) {
this.$root.dialog({
type: 'error',
text: this.$t('instance-not-registered')
});
} else {
this.instance = instance;
this.target = '';
}
});
},
fetchInstances() {
this.instances = [];
this.$root.api('federation/instances', {
state: this.state,
sort: this.sort,
limit: this.limit
}).then(instances => {
this.instances = instances;
});
},
removeAllFollowing() {
this.$root.api('admin/federation/remove-all-following', {
host: this.instance.host
}).then(() => {
this.$root.dialog({
type: 'success',
splash: true
});
});
},
updateInstance() {
this.$root.api('admin/federation/update-instance', {
host: this.instance.host,
isBlocked: this.instance.isBlocked,
});
},
setSrc(src) {
this.chartSrc = src;
},
renderChart() {
if (this.chartInstance) {
this.chartInstance.destroy();
}
this.chartInstance = new ApexCharts(this.$refs.chart, {
chart: {
type: 'area',
height: 300,
animations: {
dynamicAnimation: {
enabled: false
}
},
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
},
stroke: {
curve: 'straight',
width: 2
},
legend: {
labels: {
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
},
},
xaxis: {
type: 'datetime',
labels: {
style: {
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
}
},
axisBorder: {
color: 'rgba(0, 0, 0, 0.1)'
},
axisTicks: {
color: 'rgba(0, 0, 0, 0.1)'
},
},
yaxis: {
labels: {
formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v),
style: {
color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
}
}
},
series: this.data.series
});
this.chartInstance.render();
},
getDate(i: number) {
const y = this.now.getFullYear();
const m = this.now.getMonth();
const d = this.now.getDate();
const h = this.now.getHours();
return (
this.chartSpan == 'day' ? new Date(y, m, d - i) :
this.chartSpan == 'hour' ? new Date(y, m, d, h - i) :
null
);
},
format(arr) {
return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v }));
},
requestsChart(): any {
return {
series: [{
name: 'Incoming',
data: this.format(this.stats.requests.received)
}, {
name: 'Outgoing (succeeded)',
data: this.format(this.stats.requests.succeeded)
}, {
name: 'Outgoing (failed)',
data: this.format(this.stats.requests.failed)
}]
};
},
usersChart(total: boolean): any {
return {
series: [{
name: 'Users',
type: 'area',
data: this.format(total
? this.stats.users.total
: sum(this.stats.users.inc, negate(this.stats.users.dec))
)
}]
};
},
notesChart(total: boolean): any {
return {
series: [{
name: 'Notes',
type: 'area',
data: this.format(total
? this.stats.notes.total
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
)
}]
};
},
ffChart(total: boolean): any {
return {
series: [{
name: 'Following',
type: 'area',
data: this.format(total
? this.stats.following.total
: sum(this.stats.following.inc, negate(this.stats.following.dec))
)
}, {
name: 'Followers',
type: 'area',
data: this.format(total
? this.stats.followers.total
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
)
}]
};
},
driveUsageChart(total: boolean): any {
return {
bytes: true,
series: [{
name: 'Drive usage',
type: 'area',
data: this.format(total
? this.stats.drive.totalUsage
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
)
}]
};
},
driveFilesChart(total: boolean): any {
return {
series: [{
name: 'Drive files',
type: 'area',
data: this.format(total
? this.stats.drive.totalFiles
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
)
}]
};
},
}
});
</script>
<style lang="stylus" scoped>
.target
margin-bottom 16px !important
.instances
width 100%
> header
display flex
> *
color var(--text)
font-weight bold
> div
display flex
> * > *
flex 1
overflow auto
&:first-child
min-width 200px
</style>

View File

@ -24,7 +24,7 @@
<li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li>
<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li>
<li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li>
<!-- <li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faShareAlt" fixed-width/>{{ $t('federation') }}</li> -->
<li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</li>
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
@ -48,6 +48,7 @@
<div v-if="page == 'announcements'"><x-announcements/></div>
<div v-if="page == 'hashtags'"><x-hashtags/></div>
<div v-if="page == 'drive'"><x-drive/></div>
<div v-if="page == 'federation'"><x-federation/></div>
<div v-if="page == 'abuse'"><x-abuse/></div>
</div>
</main>
@ -68,7 +69,9 @@ import XHashtags from "./hashtags.vue";
import XUsers from "./users.vue";
import XDrive from "./drive.vue";
import XAbuse from "./abuse.vue";
import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle, faTasks } from '@fortawesome/free-solid-svg-icons';
import XFederation from "./federation.vue";
import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks } from '@fortawesome/free-solid-svg-icons';
import { faGrin } from '@fortawesome/free-regular-svg-icons';
// Detect the user agent
@ -88,6 +91,7 @@ export default Vue.extend({
XUsers,
XDrive,
XAbuse,
XFederation,
},
provide: {
isMobile
@ -101,7 +105,7 @@ export default Vue.extend({
faGrin,
faArrowLeft,
faHeadset,
faShareAlt,
faGlobe,
faExclamationCircle,
faTasks
};

View File

@ -6,8 +6,10 @@
<ui-input v-model="username" type="text">
<span slot="prefix">@</span>
</ui-input>
<ui-button @click="add" :disabled="changing">{{ $t('add-moderator.add') }}</ui-button>
<ui-button @click="remove" :disabled="changing">{{ $t('add-moderator.remove') }}</ui-button>
<ui-horizon-group>
<ui-button @click="add" :disabled="changing">{{ $t('add-moderator.add') }}</ui-button>
<ui-button @click="remove" :disabled="changing">{{ $t('add-moderator.remove') }}</ui-button>
</ui-horizon-group>
</section>
</ui-card>
</div>

View File

@ -28,7 +28,7 @@
suspendedInfoBg: '$secondary',
suspendedInfoFg: '$primary',
remoteInfoBg: '$secondary',
remoteInfoFg: '#$primary',
remoteInfoFg: '$primary',
desktopHeaderBg: '#1c2938',
desktopHeaderFg: '#a9adae',
desktopHeaderHoverFg: '#fff',

View File

@ -4,10 +4,9 @@
import * as fs from 'fs';
import { URL } from 'url';
import $ from 'cafy';
import * as yaml from 'js-yaml';
import { Source, Mixin } from './types';
import * as pkg from '../../package.json';
import { fromNullable } from '../prelude/maybe';
/**
* Path of configuration directory
@ -22,148 +21,31 @@ const path = process.env.NODE_ENV == 'test'
: `${dir}/default.yml`;
export default function load() {
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8'));
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
if (typeof config.url !== 'string') {
throw 'You need to configure the URL.';
}
const mixin = {} as Mixin;
const url = validateUrl(config.url);
if (typeof config.port !== 'number') {
throw 'You need to configure the port.';
}
config.url = normalizeUrl(config.url);
if (config.https != null) {
if (typeof config.https.key !== 'string') {
throw 'You need to configure the https key.';
}
if (typeof config.https.cert !== 'string') {
throw 'You need to configure the https cert.';
}
}
mixin.host = url.host;
mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, '');
mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
mixin.user_agent = `Misskey/${pkg.version} (${config.url})`;
if (config.mongodb == null) {
throw 'You need to configure the MongoDB.';
}
if (config.autoAdmin == null) config.autoAdmin = false;
if (typeof config.mongodb.host !== 'string') {
throw 'You need to configure the MongoDB host.';
}
if (typeof config.mongodb.port !== 'number') {
throw 'You need to configure the MongoDB port.';
}
if (typeof config.mongodb.db !== 'string') {
throw 'You need to configure the MongoDB database name.';
}
if (config.drive == null) {
throw 'You need to configure the drive.';
}
if (typeof config.drive.storage !== 'string') {
throw 'You need to configure the drive storage type.';
}
if (!$.str.or(['db', 'minio']).ok(config.drive.storage)) {
throw 'Unrecognized drive storage type is specified.';
}
if (config.drive.storage === 'minio') {
if (typeof config.drive.storage.bucket !== 'string') {
throw 'You need to configure the minio bucket.';
}
if (typeof config.drive.storage.prefix !== 'string') {
throw 'You need to configure the minio prefix.';
}
if (config.drive.storage.prefix.config == null) {
throw 'You need to configure the minio.';
}
}
if (config.redis != null) {
if (typeof config.redis.host !== 'string') {
throw 'You need to configure the Redis host.';
}
if (typeof config.redis.port !== 'number') {
throw 'You need to configure the Redis port.';
}
}
if (config.elasticsearch != null) {
if (typeof config.elasticsearch.host !== 'string') {
throw 'You need to configure the Elasticsearch host.';
}
if (typeof config.elasticsearch.port !== 'number') {
throw 'You need to configure the Elasticsearch port.';
}
}
const source = {
url: normalizeUrl(config.url as string),
port: config.port as number,
https: fromNullable(config.https).map(x => ({
key: x.key as string,
cert: x.cert as string,
ca: fromNullable<string>(x.ca)
})),
mongodb: {
host: config.mongodb.host as string,
port: config.mongodb.port as number,
db: config.mongodb.db as string,
user: fromNullable<string>(config.mongodb.user),
pass: fromNullable<string>(config.mongodb.pass)
},
redis: fromNullable(config.redis).map(x => ({
host: x.host as string,
port: x.port as number,
pass: fromNullable<string>(x.pass)
})),
elasticsearch: fromNullable(config.elasticsearch).map(x => ({
host: x.host as string,
port: x.port as number,
pass: fromNullable<string>(x.pass)
})),
disableHsts: typeof config.disableHsts === 'boolean' ? config.disableHsts as boolean : false,
drive: {
storage: config.drive.storage as string,
bucket: config.drive.bucket as string,
prefix: config.drive.prefix as string,
baseUrl: fromNullable<string>(config.drive.baseUrl),
config: config.drive.config
},
autoAdmin: typeof config.autoAdmin === 'boolean' ? config.autoAdmin as boolean : false,
proxy: fromNullable<string>(config.proxy),
clusterLimit: typeof config.clusterLimit === 'number' ? config.clusterLimit as number : Infinity,
};
const host = url.host;
const scheme = url.protocol.replace(/:$/, '');
const ws_scheme = scheme.replace('http', 'ws');
const mixin = {
host: url.host,
hostname: url.hostname,
scheme: scheme,
ws_scheme: ws_scheme,
ws_url: `${ws_scheme}://${host}`,
api_url: `${scheme}://${host}/api`,
auth_url: `${scheme}://${host}/auth`,
dev_url: `${scheme}://${host}/dev`,
docs_url: `${scheme}://${host}/docs`,
stats_url: `${scheme}://${host}/stats`,
status_url: `${scheme}://${host}/status`,
drive_url: `${scheme}://${host}/files`,
user_agent: `Misskey/${pkg.version} (${config.url})`
};
return Object.assign(source, mixin);
return Object.assign(config, mixin);
}
function tryCreateUrl(url: string) {

View File

@ -1,3 +1,64 @@
import load from "./load";
/**
* ユーザーが設定する必要のある情報
*/
export type Source = {
repository_url?: string;
feedback_url?: string;
url: string;
port: number;
https?: { [x: string]: string };
disableHsts?: boolean;
mongodb: {
host: string;
port: number;
db: string;
user: string;
pass: string;
};
redis: {
host: string;
port: number;
pass: string;
};
elasticsearch: {
host: string;
port: number;
pass: string;
};
drive?: {
storage: string;
bucket?: string;
prefix?: string;
baseUrl?: string;
config?: any;
};
export type Config = ReturnType<typeof load>;
autoAdmin?: boolean;
proxy?: string;
accesslog?: string;
clusterLimit?: number;
};
/**
* Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報
*/
export type Mixin = {
host: string;
hostname: string;
scheme: string;
ws_scheme: string;
api_url: string;
ws_url: string;
auth_url: string;
docs_url: string;
stats_url: string;
status_url: string;
dev_url: string;
drive_url: string;
user_agent: string;
};
export type Config = Source & Mixin;

View File

@ -42,10 +42,9 @@ const index = {
};
// Init ElasticSearch connection
const client = config.elasticsearch.map(({ host, port }) => {
return new elasticsearch.Client({ host: `${host}:${port}` });
}).getOrElse(null);
const client = config.elasticsearch ? new elasticsearch.Client({
host: `${config.elasticsearch.host}:${config.elasticsearch.port}`
}) : null;
if (client) {
// Send a HEAD request

View File

@ -1,7 +1,7 @@
import config from '../config';
const u = config.mongodb.user.map(x => encodeURIComponent(x)).getOrElse(null);
const p = config.mongodb.pass.map(x => encodeURIComponent(x)).getOrElse(null);
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;

View File

@ -1,8 +1,10 @@
import * as redis from 'redis';
import config from '../config';
export default config.redis.map(({ host, port, pass }) => {
return redis.createClient(port, host, {
auth_pass: pass.getOrElse(null)
});
}).getOrElse(null);
export default config.redis ? redis.createClient(
config.redis.port,
config.redis.host,
{
auth_pass: config.redis.pass
}
) : null;

View File

@ -228,7 +228,7 @@ async function init(): Promise<Config> {
return config;
}
async function spawnWorkers(limit: number) {
async function spawnWorkers(limit: number = Infinity) {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
await Promise.all([...Array(workers)].map(spawnWorker));

View File

@ -8,8 +8,8 @@ const requiredMongoDBVersion = [3, 6];
export function checkMongoDB(config: Config, logger: Logger) {
return new Promise((res, rej) => {
const mongoDBLogger = logger.createSubLogger('db');
const u = config.mongodb.user.map(x => encodeURIComponent(x)).getOrElse(null);
const p = config.mongodb.pass.map(x => encodeURIComponent(x)).getOrElse(null);
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
mongoDBLogger.info(`Connecting to ${uri} ...`);

View File

@ -21,6 +21,7 @@ export default class Logger {
private log(level: string, message: string, important = false, subDomains: string[] = []): void {
if (program.quiet) return;
if (process.env.NODE_ENV === 'test') return;
const domain = this.color ? chalk.keyword(this.color)(this.domain) : chalk.white(this.domain);
const domains = [domain].concat(subDomains);
if (this.parentLogger) {

View File

@ -11,6 +11,7 @@ DriveFile.createIndex('md5');
DriveFile.createIndex('metadata.uri');
DriveFile.createIndex('metadata.userId');
DriveFile.createIndex('metadata.folderId');
DriveFile.createIndex('metadata._user.host');
export default DriveFile;
export const DriveFileChunk = monkDb.get('driveFiles.chunks');

View File

@ -32,4 +32,44 @@ export interface IInstance {
* このインスタンスから受け取った投稿数
*/
notesCount: number;
/**
* このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
*/
followingCount: number;
/**
* このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
*/
followersCount: number;
/**
* ドライブ使用量
*/
driveUsage: number;
/**
* ドライブのファイル数
*/
driveFiles: number;
/**
* 直近のリクエスト送信日時
*/
latestRequestSentAt?: Date;
/**
* 直近のリクエスト送信時のHTTPステータスコード
*/
latestStatus?: number;
/**
* 直近のリクエスト受信日時
*/
latestRequestReceivedAt?: Date;
/**
* このインスタンスをブロックしているか
*/
isBlocked: boolean;
}

View File

@ -17,6 +17,7 @@ const User = db.get<IUser>('users');
User.createIndex('username');
User.createIndex('usernameLower');
User.createIndex('host');
User.createIndex(['username', 'host'], { unique: true });
User.createIndex(['usernameLower', 'host'], { unique: true });
User.createIndex('token', { sparse: true, unique: true });

View File

@ -1,7 +1,5 @@
export interface Maybe<T> {
isJust(): this is Just<T>;
map<S>(f: (x: T) => S): Maybe<S>;
getOrElse(x: T): T;
}
export type Just<T> = Maybe<T> & {
@ -11,8 +9,6 @@ export type Just<T> = Maybe<T> & {
export function just<T>(value: T): Just<T> {
return {
isJust: () => true,
getOrElse: (_: T) => value,
map: <S>(f: (x: T) => S) => just(f(value)),
get: () => value
};
}
@ -20,11 +16,5 @@ export function just<T>(value: T): Just<T> {
export function nothing<T>(): Maybe<T> {
return {
isJust: () => false,
getOrElse: (value: T) => value,
map: <S>(_: (x: T) => S) => nothing<S>()
};
}
export function fromNullable<T>(value: T): Maybe<T> {
return value == null ? nothing() : just(value);
}

View File

@ -8,17 +8,17 @@ import handler from './processors';
import { queueLogger } from './logger';
const enableQueue = !program.disableQueue;
const queueAvailable = config.redis.isJust();
const queueAvailable = config.redis != null;
const queue = initializeQueue();
function initializeQueue() {
return config.redis.map(({ port, host, pass }) => {
if (queueAvailable) {
return new Queue('misskey', {
redis: {
port: port,
host: host,
password: pass.getOrElse(null)
port: config.redis.port,
host: config.redis.host,
password: config.redis.pass
},
removeOnSuccess: true,
@ -27,7 +27,9 @@ function initializeQueue() {
sendEvents: false,
storeJobs: false
});
}).getOrElse(null);
} else {
return null;
}
}
export function deliver(user: ILocalUser, content: any, to: any) {

View File

@ -2,19 +2,50 @@ import * as bq from 'bee-queue';
import request from '../../../remote/activitypub/request';
import { queueLogger } from '../../logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import instanceChart from '../../../services/chart/instance';
export default async (job: bq.Job, done: any): Promise<void> => {
const { host } = new URL(job.data.to);
try {
await request(job.data.user, job.data.to, job.data.content);
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
Instance.update({ _id: i._id }, {
$set: {
latestRequestSentAt: new Date(),
latestStatus: 200
}
});
instanceChart.requestSent(i.host, true);
});
done();
} catch (res) {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
Instance.update({ _id: i._id }, {
$set: {
latestRequestSentAt: new Date(),
latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null
}
});
instanceChart.requestSent(i.host, false);
});
if (res != null && res.hasOwnProperty('statusCode')) {
queueLogger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
if (res.statusCode >= 400 && res.statusCode < 500) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく
done();
} else {
queueLogger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
done(res.statusMessage);
}
} else {

View File

@ -8,6 +8,9 @@ import { toUnicode } from 'punycode';
import { URL } from 'url';
import { publishApLogStream } from '../../../services/stream';
import Logger from '../../../misc/logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import instanceChart from '../../../services/chart/instance';
const logger = new Logger('inbox');
@ -43,6 +46,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
return;
}
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
return;
}
user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
} else {
// アクティビティ内のホストの検証
@ -55,6 +67,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
return;
}
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
return;
}
user = await User.findOne({
host: { $ne: null },
'publicKey.id': signature.keyId
@ -101,6 +122,17 @@ export default async (job: bq.Job, done: any): Promise<void> => {
});
//#endregion
// Update stats
registerOrFetchInstanceDoc(user.host).then(i => {
Instance.update({ _id: i._id }, {
$set: {
latestRequestReceivedAt: new Date()
}
});
instanceChart.requestReceived(i.host);
});
// アクティビティを処理
try {
await perform(user, activity);

View File

@ -9,10 +9,11 @@ import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'
import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta';
import { fromHtml } from '../../../mfm/fromHtml';
import usersChart from '../../../chart/users';
import usersChart from '../../../services/chart/users';
import instanceChart from '../../../services/chart/instance';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note';
import registerInstance from '../../../services/register-instance';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import getDriveFileUrl from '../../../misc/get-drive-file-url';
import { IEmoji } from '../../../models/emoji';
@ -188,15 +189,14 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
}
// Register host
registerInstance(host).then(i => {
registerOrFetchInstanceDoc(host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
usersCount: 1
}
});
// TODO
//perInstanceChart.newUser();
instanceChart.newUser(i.host);
});
//#region Increment users count

View File

@ -4,11 +4,13 @@ import { URL } from 'url';
import * as crypto from 'crypto';
import { lookup, IRunOptions } from 'lookup-dns-cache';
import * as promiseAny from 'promise-any';
import { toUnicode } from 'punycode';
import config from '../../config';
import { ILocalUser } from '../../models/user';
import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger';
import Instance from '../../models/instance';
export const logger = apLogger.createSubLogger('deliver');
@ -19,6 +21,11 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
const { protocol, host, hostname, port, pathname, search } = new URL(url);
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: toUnicode(host) });
if (instance && instance.isBlocked) return;
const data = JSON.stringify(object);
const sha256 = crypto.createHash('sha256');

View File

@ -57,7 +57,7 @@ export default class Resolver {
const object = await request({
url: value,
proxy: config.proxy.getOrElse(null),
proxy: config.proxy,
timeout: this.timeout,
headers: {
'User-Agent': config.user_agent,

View File

@ -0,0 +1,33 @@
import $ from 'cafy';
import define from '../../../define';
import Following from '../../../../../models/following';
import User from '../../../../../models/user';
import deleteFollowing from '../../../../../services/following/delete';
export const meta = {
requireCredential: true,
requireModerator: true,
params: {
host: {
validator: $.str
}
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const followings = await Following.find({
'_follower.host': ps.host
});
const pairs = await Promise.all(followings.map(f => Promise.all([
User.findOne({ _id: f.followerId }),
User.findOne({ _id: f.followeeId })
])));
for (const pair of pairs) {
deleteFollowing(pair[0], pair[1]);
}
res();
}));

View File

@ -0,0 +1,34 @@
import $ from 'cafy';
import define from '../../../define';
import Instance from '../../../../../models/instance';
export const meta = {
requireCredential: true,
requireModerator: true,
params: {
host: {
validator: $.str
},
isBlocked: {
validator: $.bool
},
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const instance = await Instance.findOne({ host: ps.host });
if (instance == null) {
return rej('instance not found');
}
Instance.update({ host: ps.host }, {
$set: {
isBlocked: ps.isBlocked
}
});
res();
}));

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import activeUsersChart from '../../../../chart/active-users';
import activeUsersChart from '../../../../services/chart/active-users';
export const meta = {
stability: 'stable',

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import driveChart from '../../../../chart/drive';
import driveChart from '../../../../services/chart/drive';
export const meta = {
stability: 'stable',

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import federationChart from '../../../../chart/federation';
import federationChart from '../../../../services/chart/federation';
export const meta = {
stability: 'stable',

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import hashtagChart from '../../../../chart/hashtag';
import hashtagChart from '../../../../services/chart/hashtag';
export const meta = {
stability: 'stable',

View File

@ -0,0 +1,42 @@
import $ from 'cafy';
import define from '../../define';
import instanceChart from '../../../../services/chart/instance';
export const meta = {
stability: 'stable',
desc: {
'ja-JP': 'インスタンスごとのチャートを取得します。'
},
params: {
span: {
validator: $.str.or(['day', 'hour']),
desc: {
'ja-JP': '集計のスパン (day または hour)'
}
},
limit: {
validator: $.num.optional.range(1, 500),
default: 30,
desc: {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
host: {
validator: $.str,
desc: {
'ja-JP': '対象のインスタンスのホスト',
'en-US': 'Target instance host'
}
}
}
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
const stats = await instanceChart.getChart(ps.span as any, ps.limit, ps.host);
res(stats);
}));

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import networkChart from '../../../../chart/network';
import networkChart from '../../../../services/chart/network';
export const meta = {
stability: 'stable',

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import notesChart from '../../../../chart/notes';
import notesChart from '../../../../services/chart/notes';
export const meta = {
stability: 'stable',

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import perUserDriveChart from '../../../../../chart/per-user-drive';
import perUserDriveChart from '../../../../../services/chart/per-user-drive';
import ID, { transform } from '../../../../../misc/cafy-id';
export const meta = {

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import perUserFollowingChart from '../../../../../chart/per-user-following';
import perUserFollowingChart from '../../../../../services/chart/per-user-following';
import ID, { transform } from '../../../../../misc/cafy-id';
export const meta = {

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import perUserNotesChart from '../../../../../chart/per-user-notes';
import perUserNotesChart from '../../../../../services/chart/per-user-notes';
import ID, { transform } from '../../../../../misc/cafy-id';
export const meta = {

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import perUserReactionsChart from '../../../../../chart/per-user-reactions';
import perUserReactionsChart from '../../../../../services/chart/per-user-reactions';
import ID, { transform } from '../../../../../misc/cafy-id';
export const meta = {

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import usersChart from '../../../../chart/users';
import usersChart from '../../../../services/chart/users';
export const meta = {
stability: 'stable',

View File

@ -0,0 +1,110 @@
import $ from 'cafy';
import define from '../../define';
import Instance from '../../../../models/instance';
export const meta = {
requireCredential: false,
params: {
state: {
validator: $.str.optional,
},
limit: {
validator: $.num.optional.range(1, 100),
default: 30
},
offset: {
validator: $.num.optional.min(0),
default: 0
},
sort: {
validator: $.str.optional,
}
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
let sort;
if (ps.sort) {
if (ps.sort == '+notes') {
sort = {
notesCount: -1
};
} else if (ps.sort == '-notes') {
sort = {
notesCount: 1
};
} else if (ps.sort == '+users') {
sort = {
usersCount: -1
};
} else if (ps.sort == '-users') {
sort = {
usersCount: 1
};
} else if (ps.sort == '+following') {
sort = {
followingCount: -1
};
} else if (ps.sort == '-following') {
sort = {
followingCount: 1
};
} else if (ps.sort == '+followers') {
sort = {
followersCount: -1
};
} else if (ps.sort == '-followers') {
sort = {
followersCount: 1
};
} else if (ps.sort == '+caughtAt') {
sort = {
caughtAt: -1
};
} else if (ps.sort == '-caughtAt') {
sort = {
caughtAt: 1
};
} else if (ps.sort == '+driveUsage') {
sort = {
driveUsage: -1
};
} else if (ps.sort == '-driveUsage') {
sort = {
driveUsage: 1
};
} else if (ps.sort == '+driveFiles') {
sort = {
driveFiles: -1
};
} else if (ps.sort == '-driveFiles') {
sort = {
driveFiles: 1
};
}
} else {
sort = {
_id: -1
};
}
const q = {} as any;
if (ps.state === 'blocked') {
q.isBlocked = true;
}
const instances = await Instance
.find(q, {
limit: ps.limit,
sort: sort,
skip: ps.offset
});
res(instances);
}));

View File

@ -0,0 +1,20 @@
import $ from 'cafy';
import define from '../../define';
import Instance from '../../../../models/instance';
export const meta = {
requireCredential: false,
params: {
host: {
validator: $.str
}
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const instance = await Instance
.findOne({ host: ps.host });
res(instance);
}));

View File

@ -1,51 +0,0 @@
import $ from 'cafy';
import define from '../define';
import Instance from '../../../models/instance';
export const meta = {
requireCredential: false,
params: {
limit: {
validator: $.num.optional.range(1, 100),
default: 30
},
offset: {
validator: $.num.optional.min(0),
default: 0
},
sort: {
validator: $.str.optional.or('+notes|-notes'),
}
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
let _sort;
if (ps.sort) {
if (ps.sort == '+notes') {
_sort = {
notesCount: -1
};
} else if (ps.sort == '-notes') {
_sort = {
notesCount: 1
};
}
} else {
_sort = {
_id: -1
};
}
const instances = await Instance
.find({}, {
limit: ps.limit,
sort: _sort,
skip: ps.offset
});
res(instances);
}));

View File

@ -46,7 +46,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
description: instance.description,
langs: instance.langs,
secure: config.https.isJust(),
secure: config.https != null,
machine: os.hostname(),
os: os.platform(),
node: process.version,
@ -83,9 +83,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline,
globalTimeLine: !instance.disableGlobalTimeline,
elasticsearch: config.elasticsearch.isJust(),
elasticsearch: config.elasticsearch ? true : false,
recaptcha: instance.enableRecaptcha,
objectStorage: config.drive.storage === 'minio',
objectStorage: config.drive && config.drive.storage === 'minio',
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,

View File

@ -6,7 +6,7 @@ import { packMany } from '../../../../models/note';
import define from '../../define';
import { countIf } from '../../../../prelude/array';
import fetchMeta from '../../../../misc/fetch-meta';
import activeUsersChart from '../../../../chart/active-users';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
export const meta = {

View File

@ -5,7 +5,7 @@ import { packMany } from '../../../../models/note';
import define from '../../define';
import { countIf } from '../../../../prelude/array';
import fetchMeta from '../../../../misc/fetch-meta';
import activeUsersChart from '../../../../chart/active-users';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
export const meta = {

View File

@ -5,7 +5,7 @@ import { getFriends } from '../../common/get-friends';
import { packMany } from '../../../../models/note';
import define from '../../define';
import { countIf } from '../../../../prelude/array';
import activeUsersChart from '../../../../chart/active-users';
import activeUsersChart from '../../../../services/chart/active-users';
import { getHideUserIds } from '../../common/get-hide-users';
export const meta = {

View File

@ -1,6 +1,6 @@
import define from '../define';
import driveChart from '../../../chart/drive';
import federationChart from '../../../chart/federation';
import driveChart from '../../../services/chart/drive';
import federationChart from '../../../services/chart/federation';
import fetchMeta from '../../../misc/fetch-meta';
export const meta = {

View File

@ -50,7 +50,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
request({
url: url,
proxy: config.proxy.getOrElse(null),
proxy: config.proxy,
timeout: timeout,
json: true,
followRedirect: true,

View File

@ -6,7 +6,7 @@ import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config';
import Meta from '../../../models/meta';
import RegistrationTicket from '../../../models/registration-tickets';
import usersChart from '../../../chart/users';
import usersChart from '../../../services/chart/users';
import fetchMeta from '../../../misc/fetch-meta';
import * as recaptcha from 'recaptcha-promise';

View File

@ -23,10 +23,10 @@ module.exports = (server: http.Server) => {
let ev: EventEmitter;
if (config.redis.isJust()) {
if (config.redis) {
// Connect to Redis
const subscriber = redis.createClient(
config.redis.get().port, config.redis.get().host);
config.redis.port, config.redis.host);
subscriber.subscribe('misskey');

View File

@ -19,7 +19,7 @@ import activityPub from './activitypub';
import nodeinfo from './nodeinfo';
import wellKnown from './well-known';
import config from '../config';
import networkChart from '../chart/network';
import networkChart from '../services/chart/network';
import apiServer from './api';
import { sum } from '../prelude/array';
import User from '../models/user';
@ -96,14 +96,13 @@ app.use(router.routes());
app.use(mount(require('./web')));
function createServer() {
if (config.https.isJust()) {
const opts = {
key: fs.readFileSync(config.https.get().key),
cert: fs.readFileSync(config.https.get().cert),
...config.https.get().ca.map<any>(path => ({ ca: fs.readFileSync(path) })).getOrElse({}),
allowHTTP1: true
};
return http2.createSecureServer(opts, app.callback()) as https.Server;
if (config.https) {
const certs: any = {};
for (const k of Object.keys(config.https)) {
certs[k] = fs.readFileSync(config.https[k]);
}
certs['allowHTTP1'] = true;
return http2.createSecureServer(certs, app.callback()) as https.Server;
} else {
return http.createServer(app.callback());
}

View File

@ -69,7 +69,7 @@ async function fetch(url: string, path: string) {
const req = request({
url: requestUrl,
proxy: config.proxy.getOrElse(null),
proxy: config.proxy,
timeout: 10 * 1000,
headers: {
'User-Agent': config.user_agent

View File

@ -31,9 +31,7 @@ const app = new Koa();
app.use(views(__dirname + '/views', {
extension: 'pug',
options: {
config: {
url: config.url
}
config
}
}));

View File

@ -8,7 +8,7 @@ import renderUndo from '../../remote/activitypub/renderer/undo';
import renderBlock from '../../remote/activitypub/renderer/block';
import { deliver } from '../../queue';
import renderReject from '../../remote/activitypub/renderer/reject';
import perUserFollowingChart from '../../chart/per-user-following';
import perUserFollowingChart from '../../services/chart/per-user-following';
import Blocking from '../../models/blocking';
export default async function(blocker: IUser, blockee: IUser) {

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from '.';
import { IUser, isLocalUser } from '../models/user';
import { IUser, isLocalUser } from '../../models/user';
/**
*

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import DriveFile, { IDriveFile } from '../models/drive-file';
import { isLocalUser } from '../models/user';
import DriveFile, { IDriveFile } from '../../models/drive-file';
import { isLocalUser } from '../../models/user';
/**
*

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from '.';
import Instance from '../models/instance';
import Instance from '../../models/instance';
/**
*

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import { IUser, isLocalUser } from '../models/user';
import db from '../db/mongodb';
import { IUser, isLocalUser } from '../../models/user';
import db from '../../db/mongodb';
/**
*

View File

@ -6,9 +6,9 @@ import * as moment from 'moment';
import * as nestedProperty from 'nested-property';
import autobind from 'autobind-decorator';
import * as mongo from 'mongodb';
import db from '../db/mongodb';
import db from '../../db/mongodb';
import { ICollection } from 'monk';
import Logger from '../misc/logger';
import Logger from '../../misc/logger';
const logger = new Logger('chart');

View File

@ -0,0 +1,302 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from '.';
import User from '../../models/user';
import Note from '../../models/note';
import Following from '../../models/following';
import DriveFile, { IDriveFile } from '../../models/drive-file';
/**
* インスタンスごとのチャート
*/
type InstanceLog = {
requests: {
/**
* 失敗したリクエスト数
*/
failed: number;
/**
* 成功したリクエスト数
*/
succeeded: number;
/**
* 受信したリクエスト数
*/
received: number;
};
notes: {
/**
* 集計期間時点での、全投稿数
*/
total: number;
/**
* 増加した投稿数
*/
inc: number;
/**
* 減少した投稿数
*/
dec: number;
};
users: {
/**
* 集計期間時点での、全ユーザー数
*/
total: number;
/**
* 増加したユーザー数
*/
inc: number;
/**
* 減少したユーザー数
*/
dec: number;
};
following: {
/**
* 集計期間時点での、全フォロー数
*/
total: number;
/**
* 増加したフォロー数
*/
inc: number;
/**
* 減少したフォロー数
*/
dec: number;
};
followers: {
/**
* 集計期間時点での、全フォロワー数
*/
total: number;
/**
* 増加したフォロワー数
*/
inc: number;
/**
* 減少したフォロワー数
*/
dec: number;
};
drive: {
/**
* 集計期間時点での、全ドライブファイル数
*/
totalFiles: number;
/**
* 集計期間時点での、全ドライブファイルの合計サイズ
*/
totalUsage: number;
/**
* 増加したドライブファイル数
*/
incFiles: number;
/**
* 増加したドライブ使用量
*/
incUsage: number;
/**
* 減少したドライブファイル数
*/
decFiles: number;
/**
* 減少したドライブ使用量
*/
decUsage: number;
};
};
class InstanceChart extends Chart<InstanceLog> {
constructor() {
super('instance', true);
}
@autobind
protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> {
const calcUsage = () => DriveFile
.aggregate([{
$match: {
'metadata._user.host': group,
'metadata.deletedAt': { $exists: false }
}
}, {
$project: {
length: true
}
}, {
$group: {
_id: null,
usage: { $sum: '$length' }
}
}])
.then(res => res.length > 0 ? res[0].usage : 0);
const [
notesCount,
usersCount,
followingCount,
followersCount,
driveFiles,
driveUsage,
] = init ? await Promise.all([
Note.count({ '_user.host': group }),
User.count({ host: group }),
Following.count({ '_follower.host': group }),
Following.count({ '_followee.host': group }),
DriveFile.count({ 'metadata._user.host': group }),
calcUsage(),
]) : [
latest ? latest.notes.total : 0,
latest ? latest.users.total : 0,
latest ? latest.following.total : 0,
latest ? latest.followers.total : 0,
latest ? latest.drive.totalFiles : 0,
latest ? latest.drive.totalUsage : 0,
];
return {
requests: {
failed: 0,
succeeded: 0,
received: 0
},
notes: {
total: notesCount,
inc: 0,
dec: 0
},
users: {
total: usersCount,
inc: 0,
dec: 0
},
following: {
total: followingCount,
inc: 0,
dec: 0
},
followers: {
total: followersCount,
inc: 0,
dec: 0
},
drive: {
totalFiles: driveFiles,
totalUsage: driveUsage,
incFiles: 0,
incUsage: 0,
decFiles: 0,
decUsage: 0
}
};
}
@autobind
public async requestReceived(host: string) {
await this.inc({
requests: {
received: 1
}
}, host);
}
@autobind
public async requestSent(host: string, isSucceeded: boolean) {
const update: Obj = {};
if (isSucceeded) {
update.succeeded = 1;
} else {
update.failed = 1;
}
await this.inc({
requests: update
}, host);
}
@autobind
public async newUser(host: string) {
await this.inc({
users: {
total: 1,
inc: 1
}
}, host);
}
@autobind
public async updateNote(host: string, isAdditional: boolean) {
await this.inc({
notes: {
total: isAdditional ? 1 : -1,
inc: isAdditional ? 1 : 0,
dec: isAdditional ? 0 : 1,
}
}, host);
}
@autobind
public async updateFollowing(host: string, isAdditional: boolean) {
await this.inc({
following: {
total: isAdditional ? 1 : -1,
inc: isAdditional ? 1 : 0,
dec: isAdditional ? 0 : 1,
}
}, host);
}
@autobind
public async updateFollowers(host: string, isAdditional: boolean) {
await this.inc({
followers: {
total: isAdditional ? 1 : -1,
inc: isAdditional ? 1 : 0,
dec: isAdditional ? 0 : 1,
}
}, host);
}
@autobind
public async updateDrive(file: IDriveFile, isAdditional: boolean) {
const update: Obj = {};
update.totalFiles = isAdditional ? 1 : -1;
update.totalUsage = isAdditional ? file.length : -file.length;
if (isAdditional) {
update.incFiles = 1;
update.incUsage = file.length;
} else {
update.decFiles = 1;
update.decUsage = file.length;
}
await this.inc({
drive: update
}, file.metadata._user.host);
}
}
export default new InstanceChart();

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from '.';
import Note, { INote } from '../models/note';
import { isLocalUser } from '../models/user';
import Note, { INote } from '../../models/note';
import { isLocalUser } from '../../models/user';
/**
* 稿

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import DriveFile, { IDriveFile } from '../models/drive-file';
import DriveFile, { IDriveFile } from '../../models/drive-file';
/**
*

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import Following from '../models/following';
import { IUser, isLocalUser } from '../models/user';
import Following from '../../models/following';
import { IUser, isLocalUser } from '../../models/user';
/**
*

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import Note, { INote } from '../models/note';
import { IUser } from '../models/user';
import Note, { INote } from '../../models/note';
import { IUser } from '../../models/user';
/**
* 稿

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator';
import Chart from './';
import { IUser, isLocalUser } from '../models/user';
import { INote } from '../models/note';
import { IUser, isLocalUser } from '../../models/user';
import { INote } from '../../models/note';
/**
*

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import User, { IUser, isLocalUser } from '../models/user';
import User, { IUser, isLocalUser } from '../../models/user';
/**
*

View File

@ -13,17 +13,19 @@ import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../mode
import DriveFolder from '../../models/drive-folder';
import { pack } from '../../models/drive-file';
import { publishMainStream, publishDriveStream } from '../stream';
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user';
import delFile from './delete-file';
import config from '../../config';
import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import driveChart from '../../chart/drive';
import perUserDriveChart from '../../chart/per-user-drive';
import driveChart from '../../services/chart/drive';
import perUserDriveChart from '../../services/chart/per-user-drive';
import instanceChart from '../../services/chart/instance';
import fetchMeta from '../../misc/fetch-meta';
import { GenerateVideoThumbnail } from './generate-video-thumbnail';
import { driveLogger } from './logger';
import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
import Instance from '../../models/instance';
const logger = driveLogger.createSubLogger('register', 'yellow');
@ -50,9 +52,8 @@ async function save(path: string, name: string, type: string, hash: string, size
if (type === 'image/webp') ext = '.webp';
}
const baseUrl = config.drive.baseUrl.getOrElse(
`${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`
);
const baseUrl = config.drive.baseUrl
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
// for original
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;
@ -524,6 +525,15 @@ export default async function(
// 統計を更新
driveChart.update(driveFile, true);
perUserDriveChart.update(driveFile, true);
if (isRemoteUser(driveFile.metadata._user)) {
instanceChart.updateDrive(driveFile, true);
Instance.update({ host: driveFile.metadata._user.host }, {
$inc: {
driveUsage: driveFile.length,
driveFiles: 1
}
});
}
return driveFile;
}

View File

@ -2,9 +2,12 @@ import * as Minio from 'minio';
import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import config from '../../config';
import driveChart from '../../chart/drive';
import perUserDriveChart from '../../chart/per-user-drive';
import driveChart from '../../services/chart/drive';
import perUserDriveChart from '../../services/chart/per-user-drive';
import instanceChart from '../../services/chart/instance';
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
import Instance from '../../models/instance';
import { isRemoteUser } from '../../models/user';
export default async function(file: IDriveFile, isExpired = false) {
if (file.metadata.storage == 'minio') {
@ -84,4 +87,13 @@ export default async function(file: IDriveFile, isExpired = false) {
// 統計を更新
driveChart.update(file, false);
perUserDriveChart.update(file, false);
if (isRemoteUser(file.metadata._user)) {
instanceChart.updateDrive(file, false);
Instance.update({ host: file.metadata._user.host }, {
$inc: {
driveUsage: -file.length,
driveFiles: -1
}
});
}
}

View File

@ -55,7 +55,7 @@ export default async (
const req = request({
url: requestUrl,
proxy: config.proxy.getOrElse(null),
proxy: config.proxy,
timeout: 10 * 1000,
headers: {
'User-Agent': config.user_agent

View File

@ -9,7 +9,10 @@ import renderAccept from '../../remote/activitypub/renderer/accept';
import renderReject from '../../remote/activitypub/renderer/reject';
import { deliver } from '../../queue';
import createFollowRequest from './requests/create';
import perUserFollowingChart from '../../chart/per-user-following';
import perUserFollowingChart from '../../services/chart/per-user-following';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import instanceChart from '../../services/chart/instance';
export default async function(follower: IUser, followee: IUser, requestId?: string) {
// check blocking
@ -97,6 +100,30 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
});
//#endregion
//#region Update instance stats
if (isRemoteUser(follower) && isLocalUser(followee)) {
registerOrFetchInstanceDoc(follower.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
followingCount: 1
}
});
instanceChart.updateFollowing(i.host, true);
});
} else if (isLocalUser(follower) && isRemoteUser(followee)) {
registerOrFetchInstanceDoc(followee.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
followersCount: 1
}
});
instanceChart.updateFollowers(i.host, true);
});
}
//#endregion
perUserFollowingChart.update(follower, followee, true);
// Publish follow event

View File

@ -5,8 +5,11 @@ import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
import perUserFollowingChart from '../../chart/per-user-following';
import perUserFollowingChart from '../../services/chart/per-user-following';
import Logger from '../../misc/logger';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import instanceChart from '../../services/chart/instance';
const logger = new Logger('following/delete');
@ -41,6 +44,30 @@ export default async function(follower: IUser, followee: IUser) {
});
//#endregion
//#region Update instance stats
if (isRemoteUser(follower) && isLocalUser(followee)) {
registerOrFetchInstanceDoc(follower.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
followingCount: -1
}
});
instanceChart.updateFollowing(i.host, false);
});
} else if (isLocalUser(follower) && isRemoteUser(followee)) {
registerOrFetchInstanceDoc(followee.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
followersCount: -1
}
});
instanceChart.updateFollowers(i.host, false);
});
}
//#endregion
perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event

View File

@ -6,7 +6,7 @@ import renderAccept from '../../../remote/activitypub/renderer/accept';
import { deliver } from '../../../queue';
import Following from '../../../models/following';
import { publishMainStream } from '../../stream';
import perUserFollowingChart from '../../../chart/per-user-following';
import perUserFollowingChart from '../../../services/chart/per-user-following';
import Logger from '../../../misc/logger';
const logger = new Logger('following/requests/accept');

View File

@ -21,13 +21,14 @@ import Meta from '../../models/meta';
import config from '../../config';
import registerHashtag from '../register-hashtag';
import isQuote from '../../misc/is-quote';
import notesChart from '../../chart/notes';
import perUserNotesChart from '../../chart/per-user-notes';
import activeUsersChart from '../../chart/active-users';
import notesChart from '../../services/chart/notes';
import perUserNotesChart from '../../services/chart/per-user-notes';
import activeUsersChart from '../../services/chart/active-users';
import instanceChart from '../../services/chart/instance';
import { erase, concat } from '../../prelude/array';
import insertNoteUnread from './unread';
import registerInstance from '../register-instance';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import extractMentions from '../../misc/extract-mentions';
import extractEmojis from '../../misc/extract-emojis';
@ -222,15 +223,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// Register host
if (isRemoteUser(user)) {
registerInstance(user.host).then(i => {
registerOrFetchInstanceDoc(user.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
notesCount: 1
}
});
// TODO
//perInstanceChart.newNote();
instanceChart.updateNote(i.host, true);
});
}
@ -500,7 +500,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
}
function index(note: INote) {
if (note.text == null || !config.elasticsearch.isJust()) return;
if (note.text == null || config.elasticsearch == null) return;
es.index({
index: 'misskey',

View File

@ -1,17 +1,20 @@
import Note, { INote } from '../../models/note';
import { IUser, isLocalUser } from '../../models/user';
import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
import { publishNoteStream } from '../stream';
import renderDelete from '../../remote/activitypub/renderer/delete';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import Following from '../../models/following';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
import notesChart from '../../chart/notes';
import perUserNotesChart from '../../chart/per-user-notes';
import notesChart from '../../services/chart/notes';
import perUserNotesChart from '../../services/chart/per-user-notes';
import config from '../../config';
import NoteUnread from '../../models/note-unread';
import read from './read';
import DriveFile from '../../models/drive-file';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import instanceChart from '../../services/chart/instance';
/**
* 投稿を削除します。
@ -91,4 +94,16 @@ export default async function(user: IUser, note: INote) {
// 統計を更新
notesChart.update(note, false);
perUserNotesChart.update(user, note, false);
if (isRemoteUser(user)) {
registerOrFetchInstanceDoc(user.host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
notesCount: -1
}
});
instanceChart.updateNote(i.host, false);
});
}
}

View File

@ -8,7 +8,7 @@ import watch from '../watch';
import renderLike from '../../../remote/activitypub/renderer/like';
import { deliver } from '../../../queue';
import { renderActivity } from '../../../remote/activitypub/renderer';
import perUserReactionsChart from '../../../chart/per-user-reactions';
import perUserReactionsChart from '../../../services/chart/per-user-reactions';
export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => {
// Myself

View File

@ -1,6 +1,6 @@
import { IUser } from '../models/user';
import Hashtag from '../models/hashtag';
import hashtagChart from '../chart/hashtag';
import hashtagChart from '../services/chart/hashtag';
export default async function(user: IUser, tag: string) {
tag = tag.toLowerCase();

View File

@ -1,7 +1,7 @@
import Instance, { IInstance } from '../models/instance';
import federationChart from '../chart/federation';
import federationChart from '../services/chart/federation';
export default async function(host: string): Promise<IInstance> {
export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> {
if (host == null) return null;
const index = await Instance.findOne({ host });

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