Compare commits

...

43 Commits

Author SHA1 Message Date
syuilo
f5be8fd313 10.93.1 2019-03-13 09:34:14 +09:00
syuilo
7f835d7f76 🎨 2019-03-13 09:32:10 +09:00
syuilo
ddbb7c5993 🎨 2019-03-13 09:26:38 +09:00
syuilo
00a3fe39e8 Update dependencies 🚀 2019-03-13 09:19:48 +09:00
syuilo
7537fb88d4 Refactor 2019-03-13 00:14:44 +09:00
syuilo
a81bc71a1e Resolve #4454 2019-03-13 00:13:56 +09:00
MeiMei
0a0aa0e2db Fix #4484 (#4485)
* Fix #4484

* import order
2019-03-12 23:38:11 +09:00
syuilo
c56b94ae96 Add type annotation to avoid type error 2019-03-12 23:31:18 +09:00
syuilo
e90712706d Add icons 🎨 2019-03-12 23:30:44 +09:00
syuilo
eb0623331f 🎨 2019-03-12 23:14:18 +09:00
MeiMei
d15bd59109 Fix queue charts (#4482) 2019-03-12 21:53:36 +09:00
syuilo
60e0b19372 10.93.0 2019-03-12 17:24:42 +09:00
syuilo
922eb937ff 🎨 2019-03-12 17:20:40 +09:00
syuilo
87573284f1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-12 17:11:33 +09:00
syuilo
a91c585f55 Add queue chart 2019-03-12 17:11:06 +09:00
syuilo
953ea21d5e New Crowdin translations (#4479)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)
2019-03-12 16:43:13 +09:00
syuilo
ecb00968bc Fix bug 2019-03-12 16:42:56 +09:00
syuilo
50ad8adb2d Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-12 13:26:33 +09:00
syuilo
16878caf09 Update doc 2019-03-12 13:26:26 +09:00
syuilo
5bc30c5493 Add log 2019-03-12 13:12:49 +09:00
MeiMei
85d89cf4c4 embedプレイヤーを閉じれるように (#4402) 2019-03-12 13:12:34 +09:00
syuilo
db693f598b シェアページを統合 2019-03-12 13:02:16 +09:00
syuilo
0494c770a1 Better share template 2019-03-12 12:59:26 +09:00
syuilo
c473b62aed Follow latest Web Share Target specification 2019-03-12 12:55:43 +09:00
syuilo
f19ac5320e Remove unnecessary checking 2019-03-12 12:46:01 +09:00
syuilo
612e3aafbc activeなジョブ数のカウント方法を分けた
https://github.com/syuilo/misskey/issues/4470#issuecomment-471827030
2019-03-12 12:31:01 +09:00
syuilo
0e97fec451 Fix typo 2019-03-12 10:46:25 +09:00
syuilo
e8c8626ee4 New Crowdin translations (#4469)
* 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 (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)
2019-03-12 10:41:05 +09:00
syuilo
d89e0f07f8 Update issue templates 2019-03-12 10:38:30 +09:00
syuilo
e7f81a42ce Resolve #4470 2019-03-12 10:35:17 +09:00
Acid Chicken (硫酸鶏)
ac614148b8 Update README.md [AUTOGEN] (#4477) 2019-03-12 09:50:42 +09:00
MeiMei
5eb02b4901 Resolve #4458 (#4476) 2019-03-12 09:50:20 +09:00
Acid Chicken (硫酸鶏)
65631525f6 Update README.md [AUTOGEN] (#4474) 2019-03-12 09:49:58 +09:00
syuilo
969435cfe9 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-12 00:34:30 +09:00
syuilo
c932f7a25b Resolve #1736 2019-03-12 00:34:19 +09:00
MeiMei
42d164dc57 log failed job (#4472) 2019-03-11 22:45:07 +09:00
syuilo
a7e60f80bd Refactor: Extract downloadTextFile function 2019-03-11 20:23:29 +09:00
syuilo
3dd5f313b7 Add icons 🎨 2019-03-11 20:12:44 +09:00
syuilo
883962c393 Add icon 🎨 2019-03-11 20:07:27 +09:00
syuilo
8a30ff1c76 🌎 A federated blogging platform 🚀 2019-03-11 20:03:02 +09:00
syuilo
e47c354916 Refactor 2019-03-11 19:57:50 +09:00
syuilo
496f42805d リストをインポートしたときにプロキシアカウントがフォローするように修正 2019-03-11 19:51:58 +09:00
syuilo
c3d34bda37 Resolve #4259 2019-03-11 19:43:58 +09:00
50 changed files with 978 additions and 279 deletions

View File

@@ -1,31 +0,0 @@
---
name: 🐛 Bug Report (🖥Client specific)
about: Create a report to help us improve
title: ''
labels: ⚠bug?, 🖥Client
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

@@ -1,31 +0,0 @@
---
name: 🐛 Bug Report (⚙Server specific)
about: Create a report to help us improve
title: ''
labels: ⚠bug?, ⚙Server
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

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

View File

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

View File

@@ -1,6 +1,24 @@
ChangeLog
=========
If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`)
10.93.1
----------
* データのエクスポートとインポートの動作を修正
* デザインの調整
10.93.0
----------
* フォローリストをインポートできるように
* embedプレイヤーを閉じれるように
* リストをインポートしたときにプロキシアカウントがフォローするように修正
* Web Share Targetの動作を修正
* おすすめアンケートのチョイスを修正
* デザインの調整
10.92.4
----------
* リストのエクスポートをできるように

View File

@@ -139,7 +139,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="@Hekovic@gyutte.site" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="Hekovic" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td>
@@ -147,14 +147,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
</tr><tr>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/user?u=18072312">@Hekovic@gyutte.site</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Sun, 10 Mar 2019 22:17:05 UTC
**Last updated:** Tue, 12 Mar 2019 00:50:06 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

View File

@@ -118,7 +118,7 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex
4. `NODE_ENV=production npm run build`
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
なにか問題が発生した場合は、`npm run clean`すると直る場合があります。
なにか問題が発生した場合は、`npm run clean`または`npm run cleanall`すると直る場合があります。
----------------------------------------------------------------

View File

@@ -301,6 +301,7 @@ common/views/pages/explore.vue:
recently-registered-users: "Nedávno registrovaní uživatelé"
popular-tags: "Populární tagy"
federated: "Z fediverse"
explore: "Prozkoumat {host}"
common/views/components/url-preview.vue:
enable-player: "Otevřít v přehrávači"
common/views/components/user-list.vue:
@@ -413,12 +414,15 @@ common/views/components/nav.vue:
feedback: "Zpětná vazba"
common/views/components/note-menu.vue:
mention: "Zmínění"
detail: "Více"
copy-content: "Zkopírovat obsah"
copy-link: "Zkopírovat odkaz"
favorite: "Přidat do oblíbených"
unfavorite: "Odebrat z oblízených"
watch: "Sledovat"
unwatch: "Přestat sledovat"
pin: "Připnout"
unpin: "Odepnout"
delete: "Odstranit"
delete-confirm: "Opravdu chcete smazat tento příspěvek?"
remote: "Ukázat originální poznámku"
@@ -555,6 +559,7 @@ common/views/components/profile-editor.vue:
email-verified: "Váš e-mail byl ověřen"
email-not-verified: "Váš email není potvrzen. Prosím zkontrolujte si svou schránku."
export: "Exportovat"
import: "Importovat"
export-targets:
following-list: "Seznam sledujících"
mute-list: "Seznam ztlumených uživatelů"
@@ -696,6 +701,7 @@ desktop/views/components/note.vue:
renote: "Renote"
add-reaction: "Přidat reakci"
undo-reaction: "Odebrat reakci"
detail: "Více"
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
desktop/views/components/notes.vue:
@@ -1031,8 +1037,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue:
not-available: "Vyhledávání je vypnuté pro tuto instanci."
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
desktop/views/pages/share.vue:
share-with: "Sdílet na {name}"
desktop/views/pages/tag.vue:
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
desktop/views/pages/user-list.users.vue:

View File

@@ -339,6 +339,7 @@ common/views/components/profile-editor.vue:
banner: "Banner"
save: "Speichern"
export: "Exportieren"
import: "Importieren"
export-targets:
user-lists: "Listen"
enter-password: "Bitte Passwort eingeben"

View File

@@ -314,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "Currently, {users} users are registered here"
common/views/components/url-preview.vue:
enable-player: "Enable playback"
disable-player: "Close the player"
common/views/components/user-list.vue:
no-users: "There are no users."
common/views/components/games/reversi/reversi.vue:
@@ -647,6 +648,8 @@ common/views/components/profile-editor.vue:
email-verified: "Your email has been verified."
email-not-verified: "Email address is not confirmed. Please check your inbox."
export: "Export"
import: "Import"
export-and-import: "Export and Import"
export-targets:
all-notes: "All posted Notes"
following-list: "List of followers"
@@ -654,6 +657,7 @@ common/views/components/profile-editor.vue:
blocking-list: "List of blocked accounts"
user-lists: "Lists"
export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
import-requested: "You have initiated an import. This may take quite some time."
enter-password: "Please enter your password"
danger-zone: "Cautious options"
delete-account: "Remove the account"
@@ -1348,8 +1352,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue:
not-available: "Search feature is turned off in the settings for this instance."
not-found: "No posts were found for '{q}'"
desktop/views/pages/share.vue:
share-with: "Share on {name}"
desktop/views/pages/tag.vue:
no-posts-found: "No posts contains \"{q}\" found."
desktop/views/pages/user-list.users.vue:

View File

@@ -421,6 +421,7 @@ common/views/components/profile-editor.vue:
save: "Guardar"
email-address: "Correo electrónico"
export: "Exportar"
import: "Importar"
export-targets:
mute-list: "Silenciar"
blocking-list: "Bloquear"

View File

@@ -522,6 +522,7 @@ common/views/components/profile-editor.vue:
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: "Exporter"
import: "Importer"
export-targets:
all-notes: "Toutes les notes publiées"
following-list: "Liste des abonnements"
@@ -1208,8 +1209,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue:
not-available: "La fonction de recherche est désactivée dans les paramètres de linstance."
not-found: "Aucune publication trouvée pour « {q} »."
desktop/views/pages/share.vue:
share-with: "Partager avec {name}"
desktop/views/pages/tag.vue:
no-posts-found: "Aucune publication contenant « {q} » na été trouvée."
desktop/views/pages/user-list.users.vue:

View File

@@ -334,6 +334,7 @@ common/views/pages/explore.vue:
common/views/components/url-preview.vue:
enable-player: "プレイヤーを開く"
disable-player: "プレイヤーを閉じる"
common/views/components/user-list.vue:
no-users: "ユーザーがいません"
@@ -701,6 +702,8 @@ common/views/components/profile-editor.vue:
email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
import: "インポート"
export-and-import: "エクスポートとインポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
@@ -708,6 +711,7 @@ common/views/components/profile-editor.vue:
blocking-list: "ブロック"
user-lists: "リスト"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
import-requested: "インポートをリクエストしました。これには時間がかかる場合があります。"
enter-password: "パスワードを入力してください"
danger-zone: "危険な設定"
delete-account: "アカウントを削除"
@@ -1176,7 +1180,7 @@ admin/views/dashboard.vue:
federated: "連合"
admin/views/queue.vue:
operation: "操作"
title: "キュー"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue:
@@ -1487,9 +1491,6 @@ desktop/views/pages/search.vue:
not-available: "検索機能はインスタンスの設定で無効になっています。"
not-found: "「{q}」に関する投稿は見つかりませんでした。"
desktop/views/pages/share.vue:
share-with: "{name}で共有"
desktop/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"

View File

@@ -476,6 +476,7 @@ common/views/components/profile-editor.vue:
email-verified: "このメールアドレスOKや"
email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
export: "エクスポート"
import: "インポート"
export-targets:
following-list: "フォロー"
mute-list: "ミュート"

View File

@@ -647,6 +647,8 @@ common/views/components/profile-editor.vue:
email-verified: "매일 주소가 확인되었습니다"
email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
export: "내보내기"
import: "가져오기"
export-and-import: "내보내기와 가져오기"
export-targets:
all-notes: "모든 글 데이터"
following-list: "팔로잉"
@@ -654,6 +656,7 @@ common/views/components/profile-editor.vue:
blocking-list: "차단"
user-lists: "리스트"
export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
import-requested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
enter-password: "비밀번호를 입력하여 주십시오"
danger-zone: "위험한 설정"
delete-account: "계정 삭제"
@@ -1348,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue:
not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다."
not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
desktop/views/pages/share.vue:
share-with: "{name}(으)로 공유"
desktop/views/pages/tag.vue:
no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
desktop/views/pages/user-list.users.vue:

View File

@@ -490,6 +490,7 @@ common/views/components/profile-editor.vue:
email-address: "Adres e-mail"
email-verified: "Twój adres e-mail został zweryfikowany."
export: "Eksportuj"
import: "Importuj"
export-targets:
following-list: "Śledzeni"
mute-list: "Wycisz"

View File

@@ -647,6 +647,8 @@ common/views/components/profile-editor.vue:
email-verified: "电子邮件地址已验证"
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
export: "导出"
import: "导入"
export-and-import: "导出/导入"
export-targets:
all-notes: "所有发帖"
following-list: "关注列表"
@@ -654,6 +656,7 @@ common/views/components/profile-editor.vue:
blocking-list: "黑名单"
user-lists: "列表"
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
import-requested: "导入请求已提交。这可能需要花一点时间。"
enter-password: "请输入您的密码"
danger-zone: "危险选项"
delete-account: "删除帐户"
@@ -1348,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue:
not-available: "在此实例的设置中关闭搜索功能。"
not-found: "没有找到“{q}”的帖子"
desktop/views/pages/share.vue:
share-with: "共享{name}"
desktop/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子"
desktop/views/pages/user-list.users.vue:

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.92.4",
"version": "10.93.1",
"codename": "nighthike",
"repository": {
"type": "git",
@@ -46,7 +46,6 @@
"@types/gulp-uglify": "3.0.6",
"@types/gulp-util": "3.0.34",
"@types/is-root": "1.0.0",
"@types/is-svg": "3.0.0",
"@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.0",
"@types/jsdom": "12.2.3",
@@ -96,7 +95,7 @@
"@types/websocket": "0.0.40",
"@types/ws": "6.0.1",
"animejs": "3.0.1",
"apexcharts": "3.5.0",
"apexcharts": "3.6.2",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
@@ -109,20 +108,20 @@
"chalk": "2.4.2",
"commander": "2.19.0",
"crc-32": "1.2.0",
"css-loader": "2.1.0",
"css-loader": "2.1.1",
"cssnano": "4.1.10",
"dateformat": "3.0.3",
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "1.0.0",
"double-ended-queue": "2.1.0-0",
"elasticsearch": "15.3.1",
"elasticsearch": "15.4.1",
"emojilib": "2.4.0",
"escape-regexp": "0.0.1",
"eslint": "5.15.0",
"eslint": "5.15.1",
"eslint-plugin-vue": "5.2.2",
"eventemitter3": "3.1.0",
"feed": "2.0.2",
"feed": "2.0.4",
"file-type": "10.9.0",
"fuckadblock": "3.2.1",
"gulp": "4.0.0",
@@ -131,20 +130,20 @@
"gulp-mocha": "6.0.0",
"gulp-rename": "1.4.0",
"gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.4",
"gulp-sourcemaps": "2.6.5",
"gulp-stylus": "2.7.0",
"gulp-tslint": "8.1.3",
"gulp-tslint": "8.1.4",
"gulp-typescript": "5.0.0",
"gulp-uglify": "3.0.1",
"gulp-uglify": "3.0.2",
"gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.13.1",
"html-minifier": "3.5.21",
"http-signature": "1.2.0",
"insert-text-at-cursor": "0.1.2",
"is-root": "2.0.0",
"is-svg": "3.0.0",
"js-yaml": "3.12.1",
"jsdom": "13.2.0",
"is-svg": "4.0.0",
"js-yaml": "3.12.2",
"jsdom": "14.0.0",
"json5": "2.1.0",
"json5-loader": "1.0.1",
"katex": "0.10.1",
@@ -189,13 +188,13 @@
"pug": "2.0.3",
"punycode": "2.1.1",
"qrcode": "1.3.3",
"randomcolor": "0.5.3",
"ratelimiter": "3.2.0",
"randomcolor": "0.5.4",
"ratelimiter": "3.3.0",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.1.10",
"redis": "2.8.0",
"request": "2.88.0",
"request-promise-native": "1.0.5",
"request-promise-native": "1.0.7",
"request-stats": "3.0.0",
"rimraf": "2.6.3",
"rndstr": "1.0.0",
@@ -210,14 +209,14 @@
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"summaly": "2.2.0",
"systeminformation": "4.0.14",
"systeminformation": "4.0.16",
"syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "1.2.3",
"textarea-caret": "3.1.0",
"tinycolor2": "1.4.1",
"tmp": "0.0.33",
"ts-loader": "5.3.3",
"ts-node": "8.0.2",
"ts-node": "8.0.3",
"tslint": "5.13.1",
"tslint-sonarts": "1.9.0",
"typescript": "3.3.3333",
@@ -232,7 +231,7 @@
"vue-color": "2.7.0",
"vue-content-loading": "1.5.3",
"vue-cropperjs": "3.0.0",
"vue-i18n": "8.8.2",
"vue-i18n": "8.9.0",
"vue-js-modal": "1.3.28",
"vue-json-pretty": "1.4.1",
"vue-loader": "15.7.0",
@@ -241,9 +240,9 @@
"vue-router": "3.0.2",
"vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.12",
"vue-svg-inline-loader": "1.2.13",
"vue-template-compiler": "2.6.8",
"vuedraggable": "2.18.1",
"vuedraggable": "2.19.2",
"vuewordcloud": "18.7.11",
"vuex": "3.1.0",
"vuex-persistedstate": "2.5.4",
@@ -252,7 +251,7 @@
"webpack": "4.28.4",
"webpack-cli": "3.2.3",
"websocket": "1.0.28",
"ws": "6.1.4",
"ws": "6.2.0",
"xev": "2.0.1"
}
}

View File

@@ -0,0 +1,194 @@
<template>
<div class="mzxlfysy">
<div>
<header>
<span><fa :icon="faInbox"/> In</span>
<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
</header>
<div ref="in"></div>
</div>
<div>
<header>
<span><fa :icon="faPaperPlane"/> Out</span>
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
</header>
<div ref="out"></div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faInbox } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import ApexCharts from 'apexcharts';
export default Vue.extend({
data() {
return {
stats: [],
inChart: null,
outChart: null,
faInbox, faPaperPlane
};
},
computed: {
latestStats(): any {
return this.stats[this.stats.length - 1];
}
},
watch: {
stats(stats) {
this.inChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
mounted() {
const chartOpts = {
chart: {
type: 'area',
height: 200,
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
},
tooltip: {
enabled: false
},
legend: {
show: false
},
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
xaxis: {
type: 'numeric',
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
show: false,
min: 0,
}
};
this.inChart = new ApexCharts(this.$refs.in, chartOpts);
this.outChart = new ApexCharts(this.$refs.out, chartOpts);
this.inChart.render();
this.outChart.render();
const connection = this.$root.stream.useSharedConnection('queueStats');
connection.on('stats', this.onStats);
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 100
});
this.$once('hook:beforeDestroy', () => {
connection.dispose();
this.inChart.destroy();
this.outChart.destroy();
});
},
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="stylus" scoped>
.mzxlfysy
display flex
> div
display block
flex 1
padding 20px 12px 0 12px
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
background var(--face)
border-radius 8px
&:first-child
margin-right 16px
> header
display flex
padding 0 8px
margin-bottom -16px
color var(--adminDashboardCardFg)
font-size 14px
> span
&:last-child
margin-left auto
opacity 0.7
> span
opacity 0.7
> div
margin-bottom -10px
@media (max-width 1000px)
display block
margin-bottom 26px
> div
&:first-child
margin-right 0
margin-bottom 26px
</style>

View File

@@ -73,6 +73,10 @@
<x-charts ref="charts"/>
</div>
<div class="queue">
<x-queue/>
</div>
<div class="cpu-memory">
<x-cpu-memory :connection="connection"/>
</div>
@@ -86,7 +90,8 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import XCpuMemory from "./cpu-memory.vue";
import XCpuMemory from "./dashboard.cpu-memory.vue";
import XQueue from "./dashboard.queue-charts.vue";
import XCharts from "./charts.vue";
import XApLog from "./ap-log.vue";
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
@@ -98,6 +103,7 @@ export default Vue.extend({
components: {
XCpuMemory,
XQueue,
XCharts,
XApLog,
MarqueeText
@@ -274,6 +280,9 @@ export default Vue.extend({
> .charts
margin-bottom 16px
> .queue
margin-bottom 16px
> .cpu-memory
margin-bottom 16px

View File

@@ -1,34 +1,58 @@
<template>
<div>
<ui-card>
<template #title>{{ $t('operation') }}</template>
<section>
<header>Deliver</header>
<ui-horizon-group inputs v-if="stats">
<ui-input :value="stats.deliver.waiting | number" type="text" readonly>
<span>Waiting</span>
<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
<section class="wptihjuy">
<header><fa :icon="faPaperPlane"/> Deliver</header>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
<span>Process</span>
<template #prefix><fa :icon="fasPlayCircle"/></template>
<template #suffix>jobs/s</template>
</ui-input>
<ui-input :value="stats.deliver.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="stats.deliver.active | number" type="text" readonly>
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
<span>Active</span>
<template #prefix><fa :icon="farPlayCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
<span>Waiting</span>
<template #prefix><fa :icon="faStopCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
<span>Delayed</span>
<template #prefix><fa :icon="faStopwatch"/></template>
<template #suffix>jobs</template>
</ui-input>
</ui-horizon-group>
<div ref="deliverChart" class="chart"></div>
</section>
<section>
<header>Inbox</header>
<ui-horizon-group inputs v-if="stats">
<ui-input :value="stats.inbox.waiting | number" type="text" readonly>
<span>Waiting</span>
<section class="wptihjuy">
<header><fa :icon="faInbox"/> Inbox</header>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
<span>Process</span>
<template #prefix><fa :icon="fasPlayCircle"/></template>
<template #suffix>jobs/s</template>
</ui-input>
<ui-input :value="stats.inbox.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="stats.inbox.active | number" type="text" readonly>
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
<span>Active</span>
<template #prefix><fa :icon="farPlayCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
<span>Waiting</span>
<template #prefix><fa :icon="faStopCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
<span>Delayed</span>
<template #prefix><fa :icon="faStopwatch"/></template>
<template #suffix>jobs</template>
</ui-input>
</ui-horizon-group>
<div ref="inboxChart" class="chart"></div>
</section>
<section>
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
@@ -40,29 +64,131 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2';
import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/queue.vue'),
data() {
return {
stats: null
stats: [],
deliverChart: null,
inboxChart: null,
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
};
},
created() {
const fetchStats = () => {
this.$root.api('admin/queue/stats', {}, true).then(stats => {
this.stats = stats;
});
computed: {
latestStats(): any {
return this.stats[this.stats.length - 1];
}
},
watch: {
stats(stats) {
this.inboxChart.updateSeries([{
name: 'Process',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
name: 'Active',
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
name: 'Waiting',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
name: 'Delayed',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.deliverChart.updateSeries([{
name: 'Process',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
name: 'Active',
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
name: 'Waiting',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
name: 'Delayed',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
mounted() {
const chartOpts = {
chart: {
type: 'area',
height: 200,
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
},
tooltip: {
enabled: false
},
legend: {
labels: {
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
},
},
series: [] as any,
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
xaxis: {
type: 'numeric',
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
show: false,
min: 0,
}
};
fetchStats();
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
const clock = setInterval(fetchStats, 1000);
this.inboxChart.render();
this.deliverChart.render();
const connection = this.$root.stream.useSharedConnection('queueStats');
connection.on('stats', this.onStats);
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 100
});
this.$once('hook:beforeDestroy', () => {
clearInterval(clock);
connection.dispose();
this.inboxChart.destroy();
this.deliverChart.destroy();
});
},
@@ -83,6 +209,24 @@ export default Vue.extend({
});
});
},
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="stylus" scoped>
.wptihjuy
> .chart
min-height 200px !important
</style>

View File

@@ -51,12 +51,12 @@
<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
</ui-input>
<ui-button @click="save(true)">{{ $t('save') }}</ui-button>
<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</ui-form>
</section>
<section>
<header>{{ $t('advanced') }}</header>
<header><fa :icon="faCogs"/> {{ $t('advanced') }}</header>
<div>
<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
@@ -66,7 +66,7 @@
</section>
<section>
<header>{{ $t('privacy') }}</header>
<header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header>
<div>
<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
@@ -76,7 +76,7 @@
</section>
<section v-if="enableEmail">
<header>{{ $t('email') }}</header>
<header><fa :icon="faEnvelope"/> {{ $t('email') }}</header>
<div>
<template v-if="$store.state.i.email != null">
@@ -84,12 +84,12 @@
<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
</template>
<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
<ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
<ui-button @click="updateEmail()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</div>
</section>
<section>
<header>{{ $t('export') }}</header>
<header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header>
<div>
<ui-select v-model="exportTarget">
@@ -99,7 +99,10 @@
<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
<option value="user-lists">{{ $t('export-targets.user-lists') }}</option>
</ui-select>
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
<ui-horizon-group class="fit-bottom">
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
<ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button>
</ui-horizon-group>
</div>
</section>
@@ -119,7 +122,8 @@ import { apiUrl, host } from '../../../../config';
import { toUnicode } from 'punycode';
import langmap from 'langmap';
import { unique } from '../../../../../../prelude/array';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons';
import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/profile-editor.vue'),
@@ -148,7 +152,7 @@ export default Vue.extend({
avatarUploading: false,
bannerUploading: false,
exportTarget: 'notes',
faDownload
faDownload, faUpload, faSave, faEnvelope, faUnlockAlt, faBoxes, faCogs
};
},
@@ -294,6 +298,22 @@ export default Vue.extend({
});
},
doImport() {
this.$chooseDriveFile().then(file => {
this.$root.api(
this.exportTarget == 'following' ? 'i/import-following' :
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
null, {
fileId: file.id
});
this.$root.dialog({
type: 'info',
text: this.$t('import-requested')
});
});
},
async deleteAccount() {
const { canceled: canceled, result: password } = await this.$root.dialog({
title: this.$t('enter-password'),

View File

@@ -1,5 +1,6 @@
<template>
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
<button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button>
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
</div>
<div v-else-if="tweetUrl && detail" class="twitter">
@@ -126,6 +127,22 @@ export default Vue.extend({
position relative
width 100%
> button
position absolute
top -1.5em
right 0
font-size 1em
width 1.5em
height 1.5em
padding 0
margin 0
color var(--text)
background rgba(128, 128, 128, 0.2)
opacity 0.7
&:hover
opacity 0.9
> iframe
height 100%
left 0

View File

@@ -3,7 +3,7 @@
<h1>{{ $t('share-with', { name }) }}</h1>
<div>
<mk-signin v-if="!$store.getters.isSignedIn"/>
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
<mk-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
<p v-if="posted" class="posted"><fa icon="check"/></p>
</div>
<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
@@ -20,9 +20,21 @@ export default Vue.extend({
return {
name: null,
posted: false,
text: new URLSearchParams(location.search).get('text')
text: new URLSearchParams(location.search).get('text'),
url: new URLSearchParams(location.search).get('url'),
title: new URLSearchParams(location.search).get('title'),
};
},
computed: {
template(): string {
let t = '';
if (this.title && this.url) t += `【[${title}](${url})】\n`;
if (this.title && !this.url) t += `${title}\n`;
if (this.text) t += `${text}\n`;
if (!this.title && this.url) t += `${url}`;
return t.trim();
}
},
methods: {
close() {
window.close();

View File

@@ -6,12 +6,12 @@
<div class="mntrproz">
<div>
<b>In</b>
<span v-if="latestStats">{{ latestStats.inbox.active | number }} / {{ latestStats.inbox.delayed | number }}</span>
<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
<div ref="in"></div>
</div>
<div>
<b>Out</b>
<span v-if="latestStats">{{ latestStats.deliver.active | number }} / {{ latestStats.deliver.delayed | number }}</span>
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
<div ref="out"></div>
</div>
</div>
@@ -42,12 +42,20 @@ export default define({
watch: {
stats(stats) {
this.inChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
@@ -81,11 +89,8 @@ export default define({
curve: 'straight',
width: 1
},
series: [{
data: [] as any
}, {
data: [] as any
}],
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
yaxis: {
min: 0,
}

View File

@@ -18,7 +18,7 @@ import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkShare from './views/pages/share.vue';
import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkNotFound from '../common/views/pages/not-found.vue';
import MkSettings from './views/pages/settings.vue';

View File

@@ -1,66 +0,0 @@
<template>
<div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr">
<h1>{{ this.$t('share-with', { name }) }}</h1>
<div>
<mk-signin v-if="!$store.getters.isSignedIn"/>
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
<p v-if="posted" class="posted"><fa icon="check"/></p>
</div>
<button v-if="posted" class="ui button" @click="close">{{ $t('@.close') }}</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('desktop/views/pages/share.vue'),
data() {
return {
name: null,
posted: false,
text: new URLSearchParams(location.search).get('text')
};
},
methods: {
close() {
window.close();
}
},
mounted() {
this.$root.getMeta().then(meta => {
this.name = meta.name;
});
}
});
</script>
<style lang="stylus" scoped>
.pptjhabgjtt7kwskbfv4y3uml6fpuhmr
padding 16px
> h1
margin 0 0 8px 0
color #555
font-size 20px
text-align center
> div
max-width 500px
margin 0 auto
background #fff
border solid 1px rgba(#000, 0.1)
border-radius 6px
overflow hidden
> .posted
display block
margin 0
padding 64px
text-align center
> button
display block
margin 16px auto
</style>

View File

@@ -26,7 +26,7 @@ import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue';
import MkReversi from './views/pages/games/reversi.vue';
import MkTag from './views/pages/tag.vue';
import MkShare from './views/pages/share.vue';
import MkShare from '../common/views/pages/share.vue';
import MkFollow from '../common/views/pages/follow.vue';
import MkNotFound from '../common/views/pages/not-found.vue';

View File

@@ -43,6 +43,11 @@
}
],
"share_target": {
"url_template": "share?text=【{title}】%0A{text}%0A{url}"
"action": "/share/",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}

View File

@@ -16,19 +16,43 @@ export default function() {
ev.emit(`queueStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50));
});
let activeDeliverJobs = 0;
let activeInboxJobs = 0;
deliverQueue.on('global:active', () => {
activeDeliverJobs++;
});
inboxQueue.on('global:active', () => {
activeInboxJobs++;
});
async function tick() {
const deliverJobCounts = await deliverQueue.getJobCounts();
const inboxJobCounts = await inboxQueue.getJobCounts();
const stats = {
deliver: deliverJobCounts,
inbox: inboxJobCounts
deliver: {
activeSincePrevTick: activeDeliverJobs,
active: deliverJobCounts.active,
waiting: deliverJobCounts.waiting,
delayed: deliverJobCounts.delayed
},
inbox: {
activeSincePrevTick: activeInboxJobs,
active: inboxJobCounts.active,
waiting: inboxJobCounts.waiting,
delayed: inboxJobCounts.delayed
}
};
ev.emit('queueStats', stats);
log.unshift(stats);
if (log.length > 200) log.pop();
activeDeliverJobs = 0;
activeInboxJobs = 0;
}
tick();

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as isSvg from 'is-svg';
import isSvg from 'is-svg';
export default function(path: string) {
try {

21
src/misc/convert-host.ts Normal file
View File

@@ -0,0 +1,21 @@
import config from '../config';
import { toUnicode, toASCII } from 'punycode';
export function getFullApAccount(username: string, host: string) {
return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`;
}
export function isSelfHost(host: string) {
if (host == null) return true;
return toApHost(config.host) === toApHost(host);
}
export function toDbHost(host: string) {
if (host == null) return null;
return toUnicode(host.toLowerCase());
}
export function toApHost(host: string) {
if (host == null) return null;
return toASCII(host.toLowerCase());
}

View File

@@ -0,0 +1,79 @@
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as util from 'util';
import chalk from 'chalk';
import * as request from 'request';
import Logger from '../services/logger';
import config from '../config';
const logger = new Logger('download-text-file');
export async function downloadTextFile(url: string): Promise<string> {
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`);
// write content at URL to temp file
await new Promise((res, rej) => {
logger.info(`Downloading ${chalk.cyan(url)} ...`);
const writable = fs.createWriteStream(path);
writable.on('finish', () => {
logger.succ(`Download finished: ${chalk.cyan(url)}`);
res();
});
writable.on('error', error => {
logger.error(`Download failed: ${chalk.cyan(url)}: ${error}`, {
url: url,
e: error
});
rej(error);
});
const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
const req = request({
url: requestUrl,
proxy: config.proxy,
timeout: 10 * 1000,
headers: {
'User-Agent': config.userAgent
}
});
req.pipe(writable);
req.on('response', response => {
if (response.statusCode !== 200) {
logger.error(`Got ${response.statusCode} (${url})`);
writable.close();
rej(response.statusCode);
}
});
req.on('error', error => {
logger.error(`Failed to start download: ${chalk.cyan(url)}: ${error}`, {
url: url,
e: error
});
writable.close();
rej(error);
});
});
logger.succ(`Downloaded to: ${path}`);
const text = await util.promisify(fs.readFile)(path, 'utf8');
cleanup();
return text;
}

View File

@@ -9,6 +9,7 @@ import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
import processDb from './processors/db';
import { queueLogger } from './logger';
import { IDriveFile } from '../models/drive-file';
function initializeQueue(name: string) {
return new Queue(name, config.redis != null ? {
@@ -33,7 +34,7 @@ deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.debug(`failed(${err}) id=${job.id} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`))
.on('error', (error) => deliverLogger.error(`error ${error}`))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
@@ -41,9 +42,9 @@ inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => inboxLogger.debug(`failed(${err}) id=${job.id}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`))
.on('error', (error) => inboxLogger.error(`error ${error}`))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id}`));
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
export function deliver(user: ILocalUser, content: any, to: any) {
if (content == null) return null;
@@ -145,6 +146,26 @@ export function createExportUserListsJob(user: ILocalUser) {
});
}
export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
return dbQueue.add('importFollowing', {
user: user,
fileId: fileId
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
return dbQueue.add('importUserLists', {
user: user,
fileId: fileId
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export default function() {
if (!program.onlyServer) {
deliverQueue.process(128, processDeliver);

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user';
import dateFormat = require('dateformat');
import Blocking from '../../../models/blocking';
import config from '../../../config';
import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-blocking');
@@ -56,7 +56,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
for (const block of blockings) {
const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user';
import dateFormat = require('dateformat');
import Following from '../../../models/following';
import config from '../../../config';
import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-following');
@@ -56,7 +56,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
for (const following of followings) {
const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user';
import dateFormat = require('dateformat');
import Mute from '../../../models/mute';
import config from '../../../config';
import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-mute');
@@ -56,7 +56,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
for (const mute of mutes) {
const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@@ -7,8 +7,8 @@ import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file';
import User from '../../../models/user';
import dateFormat = require('dateformat');
import config from '../../../config';
import UserList from '../../../models/user-list';
import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-user-lists');
@@ -46,7 +46,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
});
for (const u of users) {
const acct = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
const acct = getFullApAccount(u.username, u.host);
const content = `${list.title},${acct}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {

View File

@@ -0,0 +1,55 @@
import * as Bull from 'bull';
import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import User from '../../../models/user';
import follow from '../../../services/following/create';
import DriveFile from '../../../models/drive-file';
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user';
import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('import-following');
export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
logger.info(`Importing following of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
const file = await DriveFile.findOne({
_id: new mongo.ObjectID(job.data.fileId.toString())
});
const url = getOriginalUrl(file);
const csv = await downloadTextFile(url);
for (const line of csv.trim().split('\n')) {
const { username, host } = parseAcct(line.trim());
let target = isSelfHost(host) ? await User.findOne({
host: null,
usernameLower: username.toLowerCase()
}) : await User.findOne({
host: toDbHost(host),
usernameLower: username.toLowerCase()
});
if (host == null && target == null) continue;
if (target == null) {
target = await resolveUser(username, host);
}
logger.info(`Follow ${target._id} ...`);
follow(user, target);
}
logger.succ('Imported');
done();
}

View File

@@ -0,0 +1,70 @@
import * as Bull from 'bull';
import * as mongo from 'mongodb';
import { queueLogger } from '../../logger';
import User from '../../../models/user';
import UserList from '../../../models/user-list';
import DriveFile from '../../../models/drive-file';
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user';
import { pushUserToUserList } from '../../../services/user-list/push';
import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('import-user-lists');
export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
logger.info(`Importing user lists of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
const file = await DriveFile.findOne({
_id: new mongo.ObjectID(job.data.fileId.toString())
});
const url = getOriginalUrl(file);
const csv = await downloadTextFile(url);
for (const line of csv.trim().split('\n')) {
const listName = line.split(',')[0].trim();
const { username, host } = parseAcct(line.split(',')[1].trim());
let list = await UserList.findOne({
userId: user._id,
title: listName
});
if (list == null) {
list = await UserList.insert({
createdAt: new Date(),
userId: user._id,
title: listName,
userIds: []
});
}
let target = isSelfHost(host) ? await User.findOne({
host: null,
usernameLower: username.toLowerCase()
}) : await User.findOne({
host: toDbHost(host),
usernameLower: username.toLowerCase()
});
if (host == null && target == null) continue;
if (list.userIds.some(id => id.equals(target._id))) continue;
if (target == null) {
target = await resolveUser(username, host);
}
pushUserToUserList(target, list);
}
logger.succ('Imported');
done();
}

View File

@@ -6,6 +6,8 @@ import { exportFollowing } from './export-following';
import { exportMute } from './export-mute';
import { exportBlocking } from './export-blocking';
import { exportUserLists } from './export-user-lists';
import { importFollowing } from './import-following';
import { importUserLists } from './import-user-lists';
const jobs = {
deleteNotes,
@@ -14,7 +16,9 @@ const jobs = {
exportFollowing,
exportMute,
exportBlocking,
exportUserLists
exportUserLists,
importFollowing,
importUserLists
} as any;
export default function(dbQueue: Bull.Queue) {

View File

@@ -116,7 +116,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
: [];
// リプライ
const reply = note.inReplyTo
const reply: INote = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => {
// 4xxの場合はリプライしてないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {

View File

@@ -0,0 +1,64 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import { createImportFollowingJob } from '../../../../queue';
import ms = require('ms');
import DriveFile from '../../../../models/drive-file';
import { ApiError } from '../../error';
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 1,
},
params: {
fileId: {
validator: $.type(ID),
transform: transform,
}
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'b98644cf-a5ac-4277-a502-0b8054a709a3'
},
unexpectedFileType: {
message: 'We need csv file.',
code: 'UNEXPECTED_FILE_TYPE',
id: '660f3599-bce0-4f95-9dde-311fd841c183'
},
tooBigFile: {
message: 'That file is too big.',
code: 'TOO_BIG_FILE',
id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60'
},
emptyFile: {
message: 'That file is empty.',
code: 'EMPTY_FILE',
id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691'
},
}
};
export default define(meta, async (ps, user) => {
const file = await DriveFile.findOne({
_id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
createImportFollowingJob(user, file._id);
return;
});

View File

@@ -0,0 +1,64 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import { createImportUserListsJob } from '../../../../queue';
import ms = require('ms');
import DriveFile from '../../../../models/drive-file';
import { ApiError } from '../../error';
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 1,
},
params: {
fileId: {
validator: $.type(ID),
transform: transform,
}
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049'
},
unexpectedFileType: {
message: 'We need csv file.',
code: 'UNEXPECTED_FILE_TYPE',
id: 'a3c9edda-dd9b-4596-be6a-150ef813745c'
},
tooBigFile: {
message: 'That file is too big.',
code: 'TOO_BIG_FILE',
id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9'
},
emptyFile: {
message: 'That file is empty.',
code: 'EMPTY_FILE',
id: '99efe367-ce6e-4d44-93f8-5fae7b040356'
},
}
};
export default define(meta, async (ps, user) => {
const file = await DriveFile.findOne({
_id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile);
if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
createImportUserListsJob(user, file._id);
return;
});

View File

@@ -52,10 +52,18 @@ export default define(meta, async (ps, user) => {
$ne: user._id,
$nin: hideUserIds
},
visibility: 'public',
poll: {
$exists: true,
$ne: null
}
},
$or: [{
'poll.expiresAt': null
}, {
'poll.expiresAt': {
$gt: new Date()
}
}],
}, {
limit: ps.limit,
skip: ps.offset,

View File

@@ -1,14 +1,10 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import UserList from '../../../../../models/user-list';
import { pack as packUser, isRemoteUser, fetchProxyAccount } from '../../../../../models/user';
import { publishUserListStream } from '../../../../../services/stream';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderFollow from '../../../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../../../queue';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
import { pushUserToUserList } from '../../../../../services/user-list/push';
export const meta = {
desc: {
@@ -81,18 +77,5 @@ export default define(meta, async (ps, me) => {
}
// Push the user
await UserList.update({ _id: userList._id }, {
$push: {
userIds: user._id
}
});
publishUserListStream(userList._id, 'userAdded', await packUser(user));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (isRemoteUser(user)) {
const proxy = await fetchProxyAccount();
const content = renderActivity(renderFollow(proxy, user));
deliver(proxy, content, user.inbox);
}
pushUserToUserList(user, userList);
});

View File

@@ -18,7 +18,7 @@ html
| Misskey
block desc
meta(name='description' content='A planet of fediverse')
meta(name='description' content='✨🌎✨ A federated blogging platform ✨🚀✨')
block meta

View File

@@ -0,0 +1,23 @@
import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user';
import UserList, { IUserList } from '../../models/user-list';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import renderFollow from '../../remote/activitypub/renderer/follow';
import { publishUserListStream } from '../stream';
export async function pushUserToUserList(target: IUser, list: IUserList) {
await UserList.update({ _id: list._id }, {
$push: {
userIds: target._id
}
});
publishUserListStream(list._id, 'userAdded', await packUser(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (isRemoteUser(target)) {
const proxy = await fetchProxyAccount();
const content = renderActivity(renderFollow(proxy, target));
deliver(proxy, content, target.inbox);
}
}