Compare commits

..

66 Commits

Author SHA1 Message Date
0bf602bae6 10.82.3 2019-02-07 10:55:52 +09:00
6fc28d1df7 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-07 10:52:07 +09:00
2ef795aba8 Fix bug 2019-02-07 10:51:55 +09:00
1d2c50fc26 デフォルトでログのタイムスタンプ非表示 2019-02-07 10:51:50 +09:00
cef8aa5e7a APのジョブキュー無効化 2019-02-07 10:51:24 +09:00
edf3e75344 New Crowdin translations (#4166)
* 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 (English)
2019-02-07 10:37:36 +09:00
62835c6011 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-07 10:36:09 +09:00
60fb22cb3c Update dependencies 🚀 2019-02-07 10:35:59 +09:00
20dea3a793 Merge pull request #4174 from syuilo/dependabot/npm_and_yarn/@fortawesome/free-brands-svg-icons-5.7.1 2019-02-06 20:35:37 +00:00
aba37ae701 Merge pull request #4173 from syuilo/dependabot/npm_and_yarn/tslint-5.12.1 2019-02-06 20:26:06 +00:00
2c6e6275aa Update @fortawesome/free-brands-svg-icons requirement
Updates the requirements on [@fortawesome/free-brands-svg-icons](https://github.com/FortAwesome/Font-Awesome) to permit the latest version.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/commits/5.7.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-06 20:24:43 +00:00
20ef362854 Update tslint requirement from 5.12.0 to 5.12.1
Updates the requirements on [tslint](https://github.com/palantir/tslint) to permit the latest version.
- [Release notes](https://github.com/palantir/tslint/releases)
- [Changelog](https://github.com/palantir/tslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/palantir/tslint/commits/5.12.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-06 20:15:49 +00:00
4692aa8d9b Update README.md [AUTOGEN] (#4172) 2019-02-07 03:29:10 +09:00
f7b6dc08f7 😢 2019-02-07 02:50:03 +09:00
7dfe7005e0 Update builtin themes 2019-02-07 02:36:02 +09:00
b91de4ac12 🎨 2019-02-07 02:28:08 +09:00
d5205d7328 Refactor reaction-viewer (#4171)
* Refactor reaction-viewer

* code style

* fix
2019-02-07 02:05:49 +09:00
f44ce535fa リアクションマージン再調整 (#4169)
* リアクションマージン再調整

* fix size
2019-02-06 22:57:08 +09:00
7177fd27c8 Fix bug 2019-02-06 22:56:20 +09:00
cf304f88d4 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-06 22:49:42 +09:00
dff1d84031 Fix cofig for ci 2019-02-06 22:46:21 +09:00
96bc17aa10 Check config on load (#4170)
Co-authored-by: syuilo <syuilotan@yahoo.co.jp>
2019-02-06 22:44:55 +09:00
41ba06a5e6 Fix bug 2019-02-06 22:27:23 +09:00
d7ac0418d7 Revert "余計なマージンを削除 (#4168)"
This reverts commit 77bcb58f12.
2019-02-06 21:51:01 +09:00
f4319a9c01 Revert "[Client] リアクション一覧のマージンを調整"
This reverts commit 80ea747db6.
2019-02-06 21:50:37 +09:00
f4c4d53bbb Fix bug 2019-02-06 21:21:49 +09:00
0ed43e1bdf Fix file name 2019-02-06 21:10:37 +09:00
d25bd876cb Better file names 2019-02-06 21:10:12 +09:00
b9782397c2 Fix file ext 2019-02-06 21:07:36 +09:00
ea0abc9f71 Clean up 2019-02-06 20:57:15 +09:00
27d16c6a12 Resolve #4151 2019-02-06 20:56:48 +09:00
ede70d354e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-06 19:36:52 +09:00
66fa583f6e Update example.yml 2019-02-06 19:36:44 +09:00
77bcb58f12 余計なマージンを削除 (#4168) 2019-02-06 18:29:39 +09:00
61036e3a70 Rename clusterLog to clusterLogger (#4167) 2019-02-06 18:01:35 +09:00
bcd886c4f5 🎨 2019-02-06 17:51:33 +09:00
4d868aaf1f 🎨 2019-02-06 17:10:40 +09:00
80ea747db6 [Client] リアクション一覧のマージンを調整
Close #4160
2019-02-06 17:03:43 +09:00
960f29ce81 10.82.2 2019-02-06 15:25:47 +09:00
20ee57931f Resolve #4165 2019-02-06 15:24:59 +09:00
71ba72e796 Better logs 2019-02-06 15:06:23 +09:00
9835945ee1 Improve queue option 2019-02-06 15:01:43 +09:00
4f2d52697d Update queue setting 2019-02-06 14:53:02 +09:00
46c258d77a 10.82.1 2019-02-06 14:01:52 +09:00
3b5b3cf521 Merge branches 'develop' and 'develop' of https://github.com/syuilo/misskey into develop 2019-02-06 13:56:21 +09:00
5e0bdd8a78 New Crowdin translations (#4147)
* 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)
2019-02-06 13:56:00 +09:00
b299988bb5 Simplify comment (#4164) 2019-02-06 13:52:32 +09:00
e26bec6ab4 Improve queue configuration
Resolve #4157
Resolve #4158
2019-02-06 13:51:02 +09:00
e9955e01d6 Introduce option type (#4150)
* Introduce option type

* Improve test naming
2019-02-06 13:42:35 +09:00
1974d8f58b Add URL validation (#4148) 2019-02-06 13:37:20 +09:00
08c0be11b2 Merge pull request #4163 from syuilo/dependabot/npm_and_yarn/jsdom-13.2.0 2019-02-05 20:36:15 +00:00
87c7058494 Merge pull request #4162 from syuilo/dependabot/npm_and_yarn/@types/node-10.12.21 2019-02-05 20:31:01 +00:00
b92addffa9 Update jsdom requirement from 13.1.0 to 13.2.0
Updates the requirements on [jsdom](https://github.com/jsdom/jsdom) to permit the latest version.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/commits/13.2.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-05 20:26:19 +00:00
e8b49df842 Update @types/node requirement from 10.12.18 to 10.12.21
Updates the requirements on [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-05 20:22:20 +00:00
18fd39b335 proxyで400番台はそのステータスを返す (#4154) 2019-02-06 00:20:00 +09:00
8a11322802 Update README.md 2019-02-06 00:13:31 +09:00
31929dad61 [MFM] Better hashtag parsing: Ignore slash 2019-02-06 00:05:26 +09:00
4a41d2fddc Add logs 2019-02-06 00:01:37 +09:00
4c65b0cd6f 🎨 2019-02-05 23:45:27 +09:00
3e89dc603d Bye 'is-url' (#4113) 2019-02-05 19:54:41 +09:00
9595a56346 Revert "Update load.ts"
This reverts commit cf9e8ed39e, commit 67792fcb5e, and commit c7e8c27ce6.
2019-02-04 22:30:24 +09:00
c7e8c27ce6 Fix bug
C#っぽく使ってしまっていた。
2019-02-04 02:14:18 +09:00
67792fcb5e Update load.ts 2019-02-04 02:09:41 +09:00
353fc18f19 Merge branch 'develop' into acid-chicken-patch-10 2019-02-04 02:06:46 +09:00
cf9e8ed39e Update load.ts 2019-02-04 02:06:08 +09:00
8b71006fbe Bye 'is-url' 2019-02-04 00:09:24 +09:00
97 changed files with 1304 additions and 401 deletions

View File

@ -6,6 +6,8 @@ mongodb:
db: misskey db: misskey
user: syuilo user: syuilo
pass: '' pass: ''
drive:
storage: 'db'
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379

View File

@ -6,6 +6,8 @@ mongodb:
db: test-misskey db: test-misskey
user: admin user: admin
pass: '' pass: ''
drive:
storage: 'db'
# __REDIS__ # __REDIS__
redis: redis:
host: localhost host: localhost

View File

@ -108,5 +108,8 @@ autoAdmin: true
# port: 9200 # port: 9200
# pass: null # pass: null
# Whether disable HSTS
#disableHsts: true
# Clustering # Clustering
#clusterLimit: 1 #clusterLimit: 1

View File

@ -1,6 +1,22 @@
ChangeLog ChangeLog
========= =========
10.82.3
----------
* フォロー/ミュート/ブロックデータをエクスポート可能に
* バグ修正
* デザインの調整
* ジョブキューの動作を修正
10.82.2
----------
* ジョブキューの動作を修正
10.82.1
----------
* クラスタリング環境でのジョブキューの動作を修正
* その他の軽微な改善
10.82.0 10.82.0
---------- ----------
* 自分の投稿情報をエクスポートできるように * 自分の投稿情報をエクスポートできるように

View File

@ -61,6 +61,10 @@ Organize and store your files! Want to post a picture you have already uploaded?
...and more! Experience Misskey with your own eyes at [misskey.xyz](https://misskey.xyz) or join one of the [other instances](https://joinmisskey.github.io/) that are available. ...and more! Experience Misskey with your own eyes at [misskey.xyz](https://misskey.xyz) or join one of the [other instances](https://joinmisskey.github.io/) that are available.
:new: What's new
----------------------------------------------------------------
Please see the [Release notes](./CHANGELOG.md).
:package: Create your own instance :package: Create your own instance
---------------------------------------------------------------- ----------------------------------------------------------------
Please see the [Setup and Installation Guide](./docs/setup.en.md). Please see the [Setup and Installation Guide](./docs/setup.en.md).
@ -111,6 +115,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td> <td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=2PsbFNw0tnubZzgSXD01R6hIgncfiElG7H7HX2Y3dyo%3D" alt="nemu" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=2PsbFNw0tnubZzgSXD01R6hIgncfiElG7H7HX2Y3dyo%3D" alt="nemu" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3?token-time=2145916800&token-hash=9JtETp0X8gI280Ne1E8bxn6j4Lw5o2k4mJkICx97V_k%3D" alt="YUKIMOCHI" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3?token-time=2145916800&token-hash=9JtETp0X8gI280Ne1E8bxn6j4Lw5o2k4mJkICx97V_k%3D" alt="YUKIMOCHI" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4?token-time=2145916800&token-hash=SbdZeN5SmsuT9stD6v0jN1z0hftg0FmRiCTxysU0Ihw%3D" alt="Damillora" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8241184/39e18850e87a449e9c9a71acb3310ebd/3?token-time=2145916800&token-hash=gMq30aylxu5v3G8pRhWR5jeRBbYWEoRKjGbNeiCQz5g%3D" alt="Acid Chicken" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8241184/39e18850e87a449e9c9a71acb3310ebd/3?token-time=2145916800&token-hash=gMq30aylxu5v3G8pRhWR5jeRBbYWEoRKjGbNeiCQz5g%3D" alt="Acid Chicken" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%3D" alt="natalie" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%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/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
@ -120,6 +125,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td> <td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td> <td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td> <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
<td><a href="https://www.patreon.com/acid_chicken">Acid Chicken</a></td> <td><a href="https://www.patreon.com/acid_chicken">Acid Chicken</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td> <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/hiratake">Hiratake</a></td>
@ -136,7 +142,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Sun, 03 Feb 2019 10:13:06 UTC **Last updated:** Wed, 06 Feb 2019 18:18:05 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "Email Address" email-address: "Email Address"
email-verified: "Your email has been verified." email-verified: "Your email has been verified."
email-not-verified: "Email address is not confirmed. Please check your inbox." email-not-verified: "Email address is not confirmed. Please check your inbox."
export: "Export"
export-targets:
all-notes: "All posted Notes"
following-list: "List of followers"
mute-list: "List of muted accounts"
blocking-list: "List of blocked accounts"
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."
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "User" users: "User"
rename: "Rename list" rename: "Rename list"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "Announcements" announcements: "Announcements"
hashtags: "Hashtags" hashtags: "Hashtags"
abuse: "Abuse" abuse: "Abuse"
queue: "Job Queue"
back-to-misskey: "Back to Misskey" back-to-misskey: "Back to Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Dashboard" dashboard: "Dashboard"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "Instances" instances: "Instances"
this-instance: "This instance" this-instance: "This instance"
federated: "Federated" federated: "Federated"
admin/views/queue.vue:
operation: "Action(s)"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "Abuse" title: "Abuse"
target: "Target" target: "Target"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "Correo electrónico" email-address: "Correo electrónico"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "Usuarios" users: "Usuarios"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "Hashtags" hashtags: "Hashtags"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Volver a Misskey" back-to-misskey: "Volver a Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Panel de Control" dashboard: "Panel de Control"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "Instancias" instances: "Instancias"
this-instance: "Esta instancia" this-instance: "Esta instancia"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "Adresse de courrier électronique" email-address: "Adresse de courrier électronique"
email-verified: "Ladresse du courrier électronique a été vérifiée." 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." email-not-verified: "Adresse de courriel nest pas confirmée. Veuillez vérifier votre boite de réception."
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "Utilisateur·rice" users: "Utilisateur·rice"
rename: "Renommer la liste" rename: "Renommer la liste"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "Annonces" announcements: "Annonces"
hashtags: "Hashtags" hashtags: "Hashtags"
abuse: "Abus" abuse: "Abus"
queue: "ジョブキュー"
back-to-misskey: "Retour vers Misskey" back-to-misskey: "Retour vers Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Tableau de bord" dashboard: "Tableau de bord"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "Instances" instances: "Instances"
this-instance: "Cette instance" this-instance: "Cette instance"
federated: "Fédérées" federated: "Fédérées"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "Abus" title: "Abus"
target: "Cible" target: "Cible"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -558,7 +558,11 @@ common/views/components/profile-editor.vue:
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート" export: "エクスポート"
export-notes: "すべての投稿のエクスポート" export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。" export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
@ -1129,6 +1133,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
@ -1140,6 +1145,10 @@ admin/views/dashboard.vue:
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "このメールアドレスOKや" email-verified: "このメールアドレスOKや"
email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?" email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "知っといてや" announcements: "知っといてや"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "ワイのインスタンス" this-instance: "ワイのインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "메일 주소" email-address: "메일 주소"
email-verified: "매일 주소가 확인되었습니다" email-verified: "매일 주소가 확인되었습니다"
email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다." email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "사용자" users: "사용자"
rename: "리스트 이름 바꾸기" rename: "리스트 이름 바꾸기"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "공지사항" announcements: "공지사항"
hashtags: "해시태그" hashtags: "해시태그"
abuse: "스팸 신고" abuse: "스팸 신고"
queue: "ジョブキュー"
back-to-misskey: "Misskey로 돌아가기" back-to-misskey: "Misskey로 돌아가기"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "대시보드" dashboard: "대시보드"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "인스턴스" instances: "인스턴스"
this-instance: "이 인스턴스" this-instance: "이 인스턴스"
federated: "연합" federated: "연합"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "스팸 신고" title: "스팸 신고"
target: "대상" target: "대상"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "Adres e-mail" email-address: "Adres e-mail"
email-verified: "Twój adres e-mail został zweryfikowany." email-verified: "Twój adres e-mail został zweryfikowany."
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "Użytkownicy" users: "Użytkownicy"
rename: "Zmień nazwę listy" rename: "Zmień nazwę listy"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "Ogłoszenia" announcements: "Ogłoszenia"
hashtags: "Hashtagi" hashtags: "Hashtagi"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "メールアドレス" email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更" rename: "リスト名を変更"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "お知らせ" announcements: "お知らせ"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" title: "スパム報告"
target: "対象" target: "対象"

View File

@ -509,6 +509,13 @@ common/views/components/profile-editor.vue:
email-address: "电子邮件地址" email-address: "电子邮件地址"
email-verified: "电子邮件地址已验证" email-verified: "电子邮件地址已验证"
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。" email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
export: "导出"
export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "用户" users: "用户"
rename: "重命名列表" rename: "重命名列表"
@ -1000,6 +1007,7 @@ admin/views/index.vue:
announcements: "公告" announcements: "公告"
hashtags: "标签" hashtags: "标签"
abuse: "举报垃圾信息" abuse: "举报垃圾信息"
queue: "ジョブキュー"
back-to-misskey: "返回 Misskey" back-to-misskey: "返回 Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Dashboard" dashboard: "Dashboard"
@ -1009,6 +1017,9 @@ admin/views/dashboard.vue:
instances: "例子" instances: "例子"
this-instance: "此实例" this-instance: "此实例"
federated: "联合" federated: "联合"
admin/views/queue.vue:
operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "举报垃圾信息" title: "举报垃圾信息"
target: "目标" target: "目标"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.82.0", "version": "10.82.3",
"clientVersion": "2.0.14114", "clientVersion": "2.0.14180",
"codename": "nighthike", "codename": "nighthike",
"repository": { "repository": {
"type": "git", "type": "git",
@ -28,7 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.14", "@fortawesome/fontawesome-svg-core": "1.2.14",
"@fortawesome/free-brands-svg-icons": "5.6.3", "@fortawesome/free-brands-svg-icons": "5.7.1",
"@fortawesome/free-regular-svg-icons": "5.7.0", "@fortawesome/free-regular-svg-icons": "5.7.0",
"@fortawesome/free-solid-svg-icons": "5.6.3", "@fortawesome/free-solid-svg-icons": "5.6.3",
"@fortawesome/vue-fontawesome": "0.1.5", "@fortawesome/vue-fontawesome": "0.1.5",
@ -70,7 +70,7 @@
"@types/mkdirp": "0.5.2", "@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.5", "@types/mocha": "5.2.5",
"@types/mongodb": "3.1.19", "@types/mongodb": "3.1.19",
"@types/node": "10.12.18", "@types/node": "10.12.21",
"@types/nodemailer": "4.6.5", "@types/nodemailer": "4.6.5",
"@types/nprogress": "0.0.29", "@types/nprogress": "0.0.29",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -99,7 +99,7 @@
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
"animejs": "3.0.1", "animejs": "3.0.1",
"apexcharts": "3.2.1", "apexcharts": "3.2.2",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
@ -147,9 +147,8 @@
"insert-text-at-cursor": "0.1.1", "insert-text-at-cursor": "0.1.1",
"is-root": "2.0.0", "is-root": "2.0.0",
"is-svg": "3.0.0", "is-svg": "3.0.0",
"is-url": "1.2.4",
"js-yaml": "3.12.1", "js-yaml": "3.12.1",
"jsdom": "13.1.0", "jsdom": "13.2.0",
"json5": "2.1.0", "json5": "2.1.0",
"json5-loader": "1.0.1", "json5-loader": "1.0.1",
"katex": "0.10.0", "katex": "0.10.0",
@ -224,7 +223,7 @@
"tmp": "0.0.33", "tmp": "0.0.33",
"ts-loader": "5.3.3", "ts-loader": "5.3.3",
"ts-node": "7.0.1", "ts-node": "7.0.1",
"tslint": "5.12.0", "tslint": "5.12.1",
"tslint-sonarts": "1.9.0", "tslint-sonarts": "1.9.0",
"typescript": "3.2.4", "typescript": "3.2.4",
"typescript-eslint-parser": "21.0.2", "typescript-eslint-parser": "21.0.2",
@ -233,7 +232,7 @@
"uuid": "3.3.2", "uuid": "3.3.2",
"v-animate-css": "0.0.3", "v-animate-css": "0.0.3",
"video-thumbnail-generator": "1.1.3", "video-thumbnail-generator": "1.1.3",
"vue": "2.6.2", "vue": "2.6.3",
"vue-color": "2.7.0", "vue-color": "2.7.0",
"vue-content-loading": "1.5.3", "vue-content-loading": "1.5.3",
"vue-cropperjs": "3.0.0", "vue-cropperjs": "3.0.0",
@ -246,7 +245,7 @@
"vue-sequential-entrance": "1.1.3", "vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.10", "vue-svg-inline-loader": "1.2.10",
"vue-template-compiler": "2.6.2", "vue-template-compiler": "2.6.3",
"vuedraggable": "2.17.0", "vuedraggable": "2.17.0",
"vuewordcloud": "18.7.11", "vuewordcloud": "18.7.11",
"vuex": "3.1.0", "vuex": "3.1.0",

View File

@ -5,11 +5,18 @@ program
.version(pkg.version) .version(pkg.version)
.option('--no-daemons', 'Disable daemon processes (for debbuging)') .option('--no-daemons', 'Disable daemon processes (for debbuging)')
.option('--disable-clustering', 'Disable clustering') .option('--disable-clustering', 'Disable clustering')
.option('--disable-queue', 'Disable job queue') .option('--disable-ap-queue', 'Disable creating job queue related to ap')
.option('--disable-queue', 'Disable job queue processing')
.option('--only-queue', 'Pocessing job queue only')
.option('--quiet', 'Suppress all logs') .option('--quiet', 'Suppress all logs')
.option('--verbose', 'Enable all logs') .option('--verbose', 'Enable all logs')
.option('--with-log-time', 'Include timestamp for each logs')
.option('--slow', 'Delay all requests (for debbuging)') .option('--slow', 'Delay all requests (for debbuging)')
.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.') .option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.')
.parse(process.argv); .parse(process.argv);
/*if (process.env.MK_DISABLE_AP_QUEUE)*/ program.disableApQueue = true;
if (process.env.MK_DISABLE_QUEUE) program.disableQueue = true;
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
export { program }; export { program };

View File

@ -20,6 +20,7 @@
<ul> <ul>
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li> <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li>
<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li> <li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li>
<li @click="nav('queue')" :class="{ active: page == 'queue' }"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</li>
<li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li> <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('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('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li>
@ -40,6 +41,7 @@
<div class="page"> <div class="page">
<div v-if="page == 'dashboard'"><x-dashboard/></div> <div v-if="page == 'dashboard'"><x-dashboard/></div>
<div v-if="page == 'instance'"><x-instance/></div> <div v-if="page == 'instance'"><x-instance/></div>
<div v-if="page == 'queue'"><x-queue/></div>
<div v-if="page == 'moderators'"><x-moderators/></div> <div v-if="page == 'moderators'"><x-moderators/></div>
<div v-if="page == 'users'"><x-users/></div> <div v-if="page == 'users'"><x-users/></div>
<div v-if="page == 'emoji'"><x-emoji/></div> <div v-if="page == 'emoji'"><x-emoji/></div>
@ -58,6 +60,7 @@ import i18n from '../../i18n';
import { version } from '../../config'; import { version } from '../../config';
import XDashboard from "./dashboard.vue"; import XDashboard from "./dashboard.vue";
import XInstance from "./instance.vue"; import XInstance from "./instance.vue";
import XQueue from "./queue.vue";
import XModerators from "./moderators.vue"; import XModerators from "./moderators.vue";
import XEmoji from "./emoji.vue"; import XEmoji from "./emoji.vue";
import XAnnouncements from "./announcements.vue"; import XAnnouncements from "./announcements.vue";
@ -65,7 +68,7 @@ import XHashtags from "./hashtags.vue";
import XUsers from "./users.vue"; import XUsers from "./users.vue";
import XDrive from "./drive.vue"; import XDrive from "./drive.vue";
import XAbuse from "./abuse.vue"; import XAbuse from "./abuse.vue";
import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle, faTasks } from '@fortawesome/free-solid-svg-icons';
import { faGrin } from '@fortawesome/free-regular-svg-icons'; import { faGrin } from '@fortawesome/free-regular-svg-icons';
// Detect the user agent // Detect the user agent
@ -77,6 +80,7 @@ export default Vue.extend({
components: { components: {
XDashboard, XDashboard,
XInstance, XInstance,
XQueue,
XModerators, XModerators,
XEmoji, XEmoji,
XAnnouncements, XAnnouncements,
@ -98,7 +102,8 @@ export default Vue.extend({
faArrowLeft, faArrowLeft,
faHeadset, faHeadset,
faShareAlt, faShareAlt,
faExclamationCircle faExclamationCircle,
faTasks
}; };
}, },
methods: { methods: {

View File

@ -0,0 +1,43 @@
<template>
<div>
<ui-card>
<div slot="title">{{ $t('operation') }}</div>
<section>
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
export default Vue.extend({
i18n: i18n('admin/views/queue.vue'),
data() {
return {
};
},
methods: {
async removeAllJobs() {
const process = async () => {
await this.$root.api('admin/queue/clear');
this.$root.dialog({
type: 'success',
splash: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
},
}
});
</script>

View File

@ -420,7 +420,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -92,7 +92,13 @@
<header>{{ $t('export') }}</header> <header>{{ $t('export') }}</header>
<div> <div>
<ui-button @click="exportNotes()">{{ $t('export-notes') }}</ui-button> <ui-select v-model="exportTarget">
<option value="notes">{{ $t('export-targets.all-notes') }}</option>
<option value="following">{{ $t('export-targets.following-list') }}</option>
<option value="mute">{{ $t('export-targets.mute-list') }}</option>
<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
</ui-select>
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
</div> </div>
</section> </section>
</ui-card> </ui-card>
@ -105,6 +111,7 @@ import { apiUrl, host } from '../../../config';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import langmap from 'langmap'; import langmap from 'langmap';
import { unique } from '../../../../../prelude/array'; import { unique } from '../../../../../prelude/array';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/profile-editor.vue'), i18n: i18n('common/views/components/profile-editor.vue'),
@ -131,7 +138,9 @@ export default Vue.extend({
autoAcceptFollowed: false, autoAcceptFollowed: false,
saving: false, saving: false,
avatarUploading: false, avatarUploading: false,
bannerUploading: false bannerUploading: false,
exportTarget: 'notes',
faDownload
}; };
}, },
@ -262,8 +271,13 @@ export default Vue.extend({
}); });
}, },
exportNotes() { doExport() {
this.$root.api('i/export-notes', {}); this.$root.api(
this.exportTarget == 'notes' ? 'i/export-notes' :
this.exportTarget == 'following' ? 'i/export-following' :
this.exportTarget == 'mute' ? 'i/export-mute' :
this.exportTarget == 'blocking' ? 'i/export-blocking' :
null, {});
this.$root.dialog({ this.$root.dialog({
type: 'info', type: 'info',

View File

@ -0,0 +1,147 @@
<template>
<span
class="reaction"
:class="{ reacted: note.myReaction == reaction }"
@click="toggleReaction(reaction)"
v-if="count > 0"
v-particle="!isMe"
>
<mk-reaction-icon :reaction="reaction" ref="icon"/>
<span>{{ count }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
import Icon from './reaction-icon.vue';
import anime from 'animejs';
export default Vue.extend({
props: {
reaction: {
type: String,
required: true,
},
count: {
type: Number,
required: true,
},
note: {
type: Object,
required: true,
},
canToggle: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
isMe(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
},
},
watch: {
count() {
this.anime();
},
},
methods: {
toggleReaction() {
if (this.isMe) return;
if (!this.canToggle) return;
const oldReaction = this.note.myReaction;
if (oldReaction) {
this.$root.api('notes/reactions/delete', {
noteId: this.note.id
}).then(() => {
if (oldReaction !== this.reaction) {
this.$root.api('notes/reactions/create', {
noteId: this.note.id,
reaction: this.reaction
});
}
});
} else {
this.$root.api('notes/reactions/create', {
noteId: this.note.id,
reaction: this.reaction
});
}
},
anime() {
if (this.$store.state.device.reduceMotion) return;
if (document.hidden) return;
this.$nextTick(() => {
const rect = this.$refs.icon.$el.getBoundingClientRect();
const x = rect.left;
const y = rect.top;
const icon = new Icon({
parent: this,
propsData: {
reaction: this.reaction
}
}).$mount();
icon.$el.style.position = 'absolute';
icon.$el.style.zIndex = 100;
icon.$el.style.top = (y + window.scrollY) + 'px';
icon.$el.style.left = (x + window.scrollX) + 'px';
icon.$el.style.fontSize = window.getComputedStyle(this.$refs.icon.$el).fontSize;
document.body.appendChild(icon.$el);
anime({
targets: icon.$el,
opacity: [1, 0],
translateY: [0, -64],
duration: 1000,
easing: 'linear',
complete: () => {
icon.destroyDom();
}
});
});
},
}
});
</script>
<style lang="stylus" scoped>
.reaction
display inline-block
height 32px
margin 2px
padding 0 6px
border-radius 4px
cursor pointer
*
user-select none
pointer-events none
&.reacted
background var(--primary)
> span
color var(--primaryForeground)
&:not(.reacted)
background var(--reactionViewerButtonBg)
&:hover
background var(--reactionViewerButtonHoverBg)
> .mk-reaction-icon
font-size 1.4em
> span
font-size 1.1em
line-height 32px
vertical-align middle
color var(--text)
</style>

View File

@ -1,139 +1,37 @@
<template> <template>
<div class="mk-reactions-viewer" :class="{ isMe }"> <div class="mk-reactions-viewer" :class="{ isMe }">
<template v-if="reactions"> <x-reaction v-for="(count, reaction) in reactions" :reaction="reaction" :count="count" :note="note" :key="reaction"/>
<span :class="{ reacted: note.myReaction == 'like' }" @click="toggleReaction('like')" v-if="reactions.like" v-particle="!isMe"><mk-reaction-icon reaction="like" ref="like"/><span>{{ reactions.like }}</span></span>
<span :class="{ reacted: note.myReaction == 'love' }" @click="toggleReaction('love')" v-if="reactions.love" v-particle="!isMe"><mk-reaction-icon reaction="love" ref="love"/><span>{{ reactions.love }}</span></span>
<span :class="{ reacted: note.myReaction == 'laugh' }" @click="toggleReaction('laugh')" v-if="reactions.laugh" v-particle="!isMe"><mk-reaction-icon reaction="laugh" ref="laugh"/><span>{{ reactions.laugh }}</span></span>
<span :class="{ reacted: note.myReaction == 'hmm' }" @click="toggleReaction('hmm')" v-if="reactions.hmm" v-particle="!isMe"><mk-reaction-icon reaction="hmm" ref="hmm"/><span>{{ reactions.hmm }}</span></span>
<span :class="{ reacted: note.myReaction == 'surprise' }" @click="toggleReaction('surprise')" v-if="reactions.surprise" v-particle="!isMe"><mk-reaction-icon reaction="surprise" ref="surprise"/><span>{{ reactions.surprise }}</span></span>
<span :class="{ reacted: note.myReaction == 'congrats' }" @click="toggleReaction('congrats')" v-if="reactions.congrats" v-particle="!isMe"><mk-reaction-icon reaction="congrats" ref="congrats"/><span>{{ reactions.congrats }}</span></span>
<span :class="{ reacted: note.myReaction == 'angry' }" @click="toggleReaction('angry')" v-if="reactions.angry" v-particle="!isMe"><mk-reaction-icon reaction="angry" ref="angry"/><span>{{ reactions.angry }}</span></span>
<span :class="{ reacted: note.myReaction == 'confused' }" @click="toggleReaction('confused')" v-if="reactions.confused" v-particle="!isMe"><mk-reaction-icon reaction="confused" ref="confused"/><span>{{ reactions.confused }}</span></span>
<span :class="{ reacted: note.myReaction == 'rip' }" @click="toggleReaction('rip')" v-if="reactions.rip" v-particle="!isMe"><mk-reaction-icon reaction="rip" ref="rip"/><span>{{ reactions.rip }}</span></span>
<span :class="{ reacted: note.myReaction == 'pudding' }" @click="toggleReaction('pudding')" v-if="reactions.pudding" v-particle="!isMe"><mk-reaction-icon reaction="pudding" ref="pudding"/><span>{{ reactions.pudding }}</span></span>
</template>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import Icon from './reaction-icon.vue'; import XReaction from './reactions-viewer.reaction.vue';
import anime from 'animejs';
export default Vue.extend({ export default Vue.extend({
components: {
XReaction
},
props: { props: {
note: { note: {
type: Object, type: Object,
required: true required: true
} },
}, },
computed: { computed: {
reactions(): any { reactions(): any {
return this.note.reactionCounts; return this.note.reactionCounts;
}, },
isMe(): boolean { isMe(): boolean {
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.note.userId); return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
} },
}, },
watch: {
'reactions.like'() {
this.anime('like');
},
'reactions.love'() {
this.anime('love');
},
'reactions.laugh'() {
this.anime('laugh');
},
'reactions.hmm'() {
this.anime('hmm');
},
'reactions.surprise'() {
this.anime('surprise');
},
'reactions.congrats'() {
this.anime('congrats');
},
'reactions.angry'() {
this.anime('angry');
},
'reactions.confused'() {
this.anime('confused');
},
'reactions.rip'() {
this.anime('rip');
},
'reactions.pudding'() {
this.anime('pudding');
}
},
methods: {
toggleReaction(reaction: string) {
if (this.isMe) return;
const oldReaction = this.note.myReaction;
if (oldReaction) {
this.$root.api('notes/reactions/delete', {
noteId: this.note.id
}).then(() => {
if (oldReaction !== reaction) {
this.$root.api('notes/reactions/create', {
noteId: this.note.id,
reaction: reaction
});
}
});
} else {
this.$root.api('notes/reactions/create', {
noteId: this.note.id,
reaction: reaction
});
}
},
anime(reaction: string) {
if (this.$store.state.device.reduceMotion) return;
if (document.hidden) return;
this.$nextTick(() => {
const rect = this.$refs[reaction].$el.getBoundingClientRect();
const x = rect.left;
const y = rect.top;
const icon = new Icon({
parent: this,
propsData: {
reaction: reaction
}
}).$mount();
icon.$el.style.position = 'absolute';
icon.$el.style.zIndex = 100;
icon.$el.style.top = (y + window.scrollY) + 'px';
icon.$el.style.left = (x + window.scrollX) + 'px';
icon.$el.style.fontSize = window.getComputedStyle(this.$refs[reaction].$el).fontSize;
document.body.appendChild(icon.$el);
anime({
targets: icon.$el,
opacity: [1, 0],
translateY: [0, -64],
duration: 1000,
easing: 'linear',
complete: () => {
icon.destroyDom();
}
});
});
}
}
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-reactions-viewer .mk-reactions-viewer
margin 6px 0 margin: 4px -2px
&:empty &:empty
display none display none
@ -144,38 +42,4 @@ export default Vue.extend({
&:hover &:hover
background var(--reactionViewerButtonBg) !important background var(--reactionViewerButtonBg) !important
> span
display inline-block
height 32px
margin-right 6px
padding 0 6px
border-radius 4px
cursor pointer
*
user-select none
pointer-events none
&.reacted
background var(--primary)
> span
color var(--primaryForeground)
&:not(.reacted)
background var(--reactionViewerButtonBg)
&:hover
background var(--reactionViewerButtonHoverBg)
> .mk-reaction-icon
font-size 1.4em
> span
font-size 1.1em
line-height 32px
vertical-align middle
color var(--text)
</style> </style>

View File

@ -74,7 +74,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -119,11 +119,11 @@ export default Vue.extend({
font-size 16px font-size 16px
cursor pointer cursor pointer
transition inherit transition inherit
color var(--text)
> span > span
display block display block
line-height 20px line-height 20px
color var(--text)
transition inherit transition inherit
> p > p

View File

@ -117,7 +117,7 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -86,7 +86,7 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -88,7 +88,7 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -12,7 +12,7 @@ export const hostname = address.hostname;
export const url = address.origin; export const url = address.origin;
export const apiUrl = url + '/api'; export const apiUrl = url + '/api';
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
export const lang = localStorage.getItem('lang'); export const lang = localStorage.getItem('lang') || window.lang; // windowは後方互換性のため
export const langs = _LANGS_; export const langs = _LANGS_;
export const locale = JSON.parse(localStorage.getItem('locale')); export const locale = JSON.parse(localStorage.getItem('locale'));
export const copyright = _COPYRIGHT_; export const copyright = _COPYRIGHT_;

View File

@ -78,7 +78,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -120,13 +120,13 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -598,6 +598,7 @@ export default Vue.extend({
padding 16px 0 0 0 padding 16px 0 0 0
overflow auto overflow auto
z-index 1 z-index 1
font-size 15px
&.inWindow &.inWindow
box-shadow var(--shadowRight) box-shadow var(--shadowRight)

View File

@ -218,6 +218,6 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
</style> </style>

View File

@ -74,7 +74,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -66,7 +66,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -98,7 +98,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -132,7 +132,7 @@ export default Vue.extend({
padding 0 12px padding 0 12px
text-align center text-align center
font-size 0.8em font-size 0.8em
color #aaa color var(--text)
> .instance > .instance
box-shadow var(--shadow) box-shadow var(--shadow)

View File

@ -5,7 +5,7 @@
<button slot="func" :title="$t('title')" @click="fetch"> <button slot="func" :title="$t('title')" @click="fetch">
<fa v-if="!fetching && more" icon="arrow-right"/> <fa v-if="!fetching && more" icon="arrow-right"/>
<fa v-if="!fetching && !more" icon="sync"/> <fa v-if="!fetching && !more" icon="sync"/>
</button> </button>
<div class="mkw-polls--body"> <div class="mkw-polls--body">
<div class="poll" v-if="!fetching && poll != null"> <div class="poll" v-if="!fetching && poll != null">
@ -92,13 +92,13 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -89,13 +89,13 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -129,13 +129,13 @@ export default define({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -83,13 +83,13 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -88,6 +88,6 @@ export default Vue.extend({
> .mk-time > .mk-time
display inline-block display inline-block
padding 8px padding 8px
color #aaa color var(--text)
</style> </style>

View File

@ -184,7 +184,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .placeholder > .placeholder
padding 16px padding 16px

View File

@ -171,6 +171,7 @@ export default Vue.extend({
overflow auto overflow auto
-webkit-overflow-scrolling touch -webkit-overflow-scrolling touch
background var(--secondary) background var(--secondary)
font-size 15px
.me .me
display block display block

View File

@ -121,13 +121,13 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> .fetching > .fetching
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> [data-icon] > [data-icon]
margin-right 4px margin-right 4px

View File

@ -57,7 +57,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -48,7 +48,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -52,7 +52,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -89,7 +89,7 @@ export default Vue.extend({
margin 0 margin 0
padding 16px padding 16px
text-align center text-align center
color #aaa color var(--text)
> i > i
margin-right 4px margin-right 4px

View File

@ -23,6 +23,7 @@ export const colorfulTheme: Theme = require('../theme/colorful.json5');
export const rainyTheme: Theme = require('../theme/rainy.json5'); export const rainyTheme: Theme = require('../theme/rainy.json5');
export const mauveTheme: Theme = require('../theme/mauve.json5'); export const mauveTheme: Theme = require('../theme/mauve.json5');
export const grayTheme: Theme = require('../theme/gray.json5'); export const grayTheme: Theme = require('../theme/gray.json5');
export const tweetDeckTheme: Theme = require('../theme/tweet-deck.json5');
export const builtinThemes = [ export const builtinThemes = [
lightTheme, lightTheme,
@ -38,6 +39,7 @@ export const builtinThemes = [
rainyTheme, rainyTheme,
mauveTheme, mauveTheme,
grayTheme, grayTheme,
tweetDeckTheme,
]; ];
export function applyTheme(theme: Theme, persisted = true) { export function applyTheme(theme: Theme, persisted = true) {

View File

@ -2,7 +2,7 @@
id: '2b0a0654-cdb4-4c9a-8244-736b647d3c2a', id: '2b0a0654-cdb4-4c9a-8244-736b647d3c2a',
name: 'Japanese Sushi Set', name: 'Japanese Sushi Set',
author: 'noizenecio & syuilo', author: 'Noizenecio',
base: 'dark', base: 'dark',

View File

@ -2,7 +2,7 @@
id: '252b2caf-86c2-4c3f-a73f-e1fc1cfa5298', id: '252b2caf-86c2-4c3f-a73f-e1fc1cfa5298',
name: 'Mauve', name: 'Mauve',
author: 'とわこ & syuilo', author: 'とわこ',
base: 'dark', base: 'dark',

View File

@ -2,7 +2,7 @@
id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36', id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
name: 'Lavender', name: 'Lavender',
author: 'sokuyuku & syuilo', author: 'sokuyuku',
base: 'light', base: 'light',

View File

@ -3,6 +3,7 @@
name: 'Rainy', name: 'Rainy',
author: 'syuilo', author: 'syuilo',
desc: 'It\'s a rainy day.',
base: 'light', base: 'light',

View File

@ -0,0 +1,44 @@
{
name: 'Tweet Deck',
id: '06f82fb4-0dad-4d70-8a3f-56cae91e1163',
author: 'simirall',
desc: 'Tweet like a pro.',
base: 'dark',
vars: {
primary: '#1da1f2',
secondary: '#15202b',
text: '#fdfdfd',
},
props: {
bg: '#10171e',
faceHeader: '$secondary',
faceTextButton: '$primary',
renoteGradient: '$secondary',
renoteText: '#17bf63',
quoteBorder: '#38444d',
noteHeaderAdminFg: '$primary',
noteHeaderAdminBg: '$secondary',
noteActionsReplyHover: '$primary',
noteActionsRenoteHover: '#17bf63',
noteActionsReactionHover: '#e0245e',
calendarWeek: '$primary',
calendarSaturdayOrSunday: '#e0245e',
announcementsBg: '$secondary',
announcementsTitle: '$primary',
suspendedInfoBg: '$secondary',
suspendedInfoFg: '$primary',
remoteInfoBg: '$secondary',
remoteInfoFg: '#$primary',
desktopHeaderBg: '#1c2938',
desktopHeaderFg: '#a9adae',
desktopHeaderHoverFg: '#fff',
desktopPostFormTransparentButtonFg: '#a9adae',
desktopTimelineSrc: '$primary',
desktopTimelineSrcHover: '#fff',
deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.0)',
reversiBannerGradientStart: '$primary',
reversiBannerGradientEnd: '$primary',
reversiGameEmptyCellMyTurn: ':lighten<5<$primary',
reversiGameEmptyCellCanPut: ':lighten<4<$primary',
},
}

View File

@ -4,10 +4,10 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { URL } from 'url'; import { URL } from 'url';
import $ from 'cafy';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Source, Mixin } from './types';
import isUrl = require('is-url');
import * as pkg from '../../package.json'; import * as pkg from '../../package.json';
import { fromNullable } from '../prelude/maybe';
/** /**
* Path of configuration directory * Path of configuration directory
@ -22,33 +22,163 @@ const path = process.env.NODE_ENV == 'test'
: `${dir}/default.yml`; : `${dir}/default.yml`;
export default function load() { export default function load() {
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8'));
const mixin = {} as Mixin; if (typeof config.url !== 'string') {
throw 'You need to configure the URL.';
}
// Validate URLs const url = validateUrl(config.url);
if (!isUrl(config.url)) throw `url="${config.url}" is not a valid URL`;
const url = new URL(config.url); if (typeof config.port !== 'number') {
config.url = normalizeUrl(config.url); throw 'You need to configure the port.';
}
mixin.host = url.host; if (config.https != null) {
mixin.hostname = url.hostname; if (typeof config.https.key !== 'string') {
mixin.scheme = url.protocol.replace(/:$/, ''); throw 'You need to configure the https key.';
mixin.ws_scheme = mixin.scheme.replace('http', 'ws'); }
mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`; if (typeof config.https.cert !== 'string') {
mixin.api_url = `${mixin.scheme}://${mixin.host}/api`; throw 'You need to configure the https cert.';
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.autoAdmin == null) config.autoAdmin = false; if (config.mongodb == null) {
throw 'You need to configure the MongoDB.';
}
return Object.assign(config, mixin); 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);
}
function tryCreateUrl(url: string) {
try {
return new URL(url);
} catch (e) {
throw `url="${url}" is not a valid URL.`;
}
}
function validateUrl(url: string) {
const result = tryCreateUrl(url);
if (result.pathname.replace('/', '').length) throw `url="${url}" is not a valid URL, has a pathname.`;
if (!url.includes(result.host)) throw `url="${url}" is not a valid URL, has an invalid hostname.`;
return result;
} }
function normalizeUrl(url: string) { function normalizeUrl(url: string) {

View File

@ -1,64 +1,3 @@
/** 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;
};
autoAdmin?: boolean; export type Config = ReturnType<typeof load>;
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,9 +42,10 @@ const index = {
}; };
// Init ElasticSearch connection // Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({
host: `${config.elasticsearch.host}:${config.elasticsearch.port}` const client = config.elasticsearch.map(({ host, port }) => {
}) : null; return new elasticsearch.Client({ host: `${host}:${port}` });
}).getOrElse(null);
if (client) { if (client) {
// Send a HEAD request // Send a HEAD request

View File

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

View File

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

View File

@ -26,7 +26,7 @@ import { showMachineInfo } from './misc/show-machine-info';
const logger = new Logger('core', 'cyan'); const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta'); const bootLogger = logger.createSubLogger('boot', 'magenta');
const clusterLog = logger.createSubLogger('cluster', 'orange'); const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev(); const ev = new Xev();
/** /**
@ -35,6 +35,11 @@ const ev = new Xev();
function main() { function main() {
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
if (program.onlyQueue) {
queueMain();
return;
}
if (cluster.isMaster || program.disableClustering) { if (cluster.isMaster || program.disableClustering) {
masterMain(); masterMain();
@ -53,12 +58,7 @@ function main() {
} }
} }
/** function greet() {
* Init master process
*/
async function masterMain() {
let config: Config;
if (!program.quiet) { if (!program.quiet) {
//#region Misskey logo //#region Misskey logo
const v = `v${pkg.version}`; const v = `v${pkg.version}`;
@ -75,10 +75,34 @@ async function masterMain() {
bootLogger.info('Welcome to Misskey!'); bootLogger.info('Welcome to Misskey!');
bootLogger.info(`Misskey v${pkg.version}`, true); bootLogger.info(`Misskey v${pkg.version}`, true);
bootLogger.info('Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); bootLogger.info('Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
}
/**
* Init master process
*/
async function masterMain() {
greet();
let config: Config;
try { try {
// initialize app // initialize app
config = await init(); config = await init();
if (config.port == null) {
bootLogger.error('The port is not configured. Please configure port.', true);
process.exit(1);
}
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
bootLogger.error('You need root privileges to listen on well-known port on Linux', true);
process.exit(1);
}
if (!await isPortAvailable(config.port)) {
bootLogger.error(`Port ${config.port} is already in use`, true);
process.exit(1);
}
} catch (e) { } catch (e) {
bootLogger.error('Fatal error occurred during initialization', true); bootLogger.error('Fatal error occurred during initialization', true);
process.exit(1); process.exit(1);
@ -90,6 +114,9 @@ async function masterMain() {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
// start queue
require('./queue').default();
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true); bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true);
} }
@ -100,15 +127,35 @@ async function workerMain() {
// start server // start server
await require('./server').default(); await require('./server').default();
// start processor
require('./queue').default();
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send('ready'); process.send('ready');
} }
} }
async function queueMain() {
greet();
try {
// initialize app
await init();
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', true);
process.exit(1);
}
bootLogger.succ('Misskey initialized');
// start processor
const queue = require('./queue').default();
if (queue) {
bootLogger.succ('Queue started', true);
} else {
bootLogger.error('Queue not available');
}
}
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
const requiredNodejsVersion = [10, 0, 0]; const requiredNodejsVersion = [10, 0, 0];
const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
@ -170,21 +217,6 @@ async function init(): Promise<Config> {
configLogger.succ('Loaded'); configLogger.succ('Loaded');
if (config.port == null) {
bootLogger.error('The port is not configured. Please configure port.', true);
process.exit(1);
}
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
bootLogger.error('You need root privileges to listen on well-known port on Linux', true);
process.exit(1);
}
if (!await isPortAvailable(config.port)) {
bootLogger.error(`Port ${config.port} is already in use`, true);
process.exit(1);
}
// Try to connect to MongoDB // Try to connect to MongoDB
try { try {
await checkMongoDB(config, bootLogger); await checkMongoDB(config, bootLogger);
@ -196,7 +228,7 @@ async function init(): Promise<Config> {
return config; return config;
} }
async function spawnWorkers(limit: number = Infinity) { async function spawnWorkers(limit: number) {
const workers = Math.min(limit, os.cpus().length); const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
await Promise.all([...Array(workers)].map(spawnWorker)); await Promise.all([...Array(workers)].map(spawnWorker));
@ -217,19 +249,19 @@ function spawnWorker(): Promise<void> {
// Listen new workers // Listen new workers
cluster.on('fork', worker => { cluster.on('fork', worker => {
clusterLog.debug(`Process forked: [${worker.id}]`); clusterLogger.debug(`Process forked: [${worker.id}]`);
}); });
// Listen online workers // Listen online workers
cluster.on('online', worker => { cluster.on('online', worker => {
clusterLog.debug(`Process is now online: [${worker.id}]`); clusterLogger.debug(`Process is now online: [${worker.id}]`);
}); });
// Listen for dying workers // Listen for dying workers
cluster.on('exit', worker => { cluster.on('exit', worker => {
// Replace the dead worker, // Replace the dead worker,
// we're not sentimental // we're not sentimental
clusterLog.error(chalk.red(`[${worker.id}] died :(`)); clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
cluster.fork(); cluster.fork();
}); });

View File

@ -142,7 +142,7 @@ export const mfmLanguage = P.createLanguage({
}, },
hashtag: () => P((input, i) => { hashtag: () => P((input, i) => {
const text = input.substr(i); const text = input.substr(i);
const match = text.match(/^#([^\s\.,!\?'"#:]+)/i); const match = text.match(/^#([^\s\.,!\?'"#:\/]+)/i);
if (!match) return P.makeFailure(i, 'not a hashtag'); if (!match) return P.makeFailure(i, 'not a hashtag');
let hashtag = match[1]; let hashtag = match[1];
hashtag = removeOrphanedBrackets(hashtag); hashtag = removeOrphanedBrackets(hashtag);

View File

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

View File

@ -28,7 +28,8 @@ export default class Logger {
} else { } else {
const time = dateformat(new Date(), 'HH:MM:ss'); const time = dateformat(new Date(), 'HH:MM:ss');
const process = cluster.isMaster ? '*' : cluster.worker.id; const process = cluster.isMaster ? '*' : cluster.worker.id;
const log = `${chalk.gray(time)} ${level} ${process}\t[${domains.join(' ')}]\t${message}`; let log = `${level} ${process}\t[${domains.join(' ')}]\t${message}`;
if (program.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log); console.log(important ? chalk.bold(log) : log);
} }
} }
@ -45,7 +46,7 @@ export default class Logger {
this.log(important ? chalk.bgGreen.white('DONE') : chalk.green('DONE'), chalk.green(message), important); this.log(important ? chalk.bgGreen.white('DONE') : chalk.green('DONE'), chalk.green(message), important);
} }
public debug(message: string, important = false): void { // デバッグ用に使う(開発者にとっては必要だが利用者にとっては不要な情報) public debug(message: string, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
if (process.env.NODE_ENV != 'production' || program.verbose) { if (process.env.NODE_ENV != 'production' || program.verbose) {
this.log(chalk.gray('VERB'), chalk.gray(message), important); this.log(chalk.gray('VERB'), chalk.gray(message), important);
} }

30
src/prelude/maybe.ts Normal file
View File

@ -0,0 +1,30 @@
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> & {
get(): 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
};
}
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

@ -1,21 +1,24 @@
import * as Queue from 'bee-queue'; import * as Queue from 'bee-queue';
import config from '../config'; import * as httpSignature from 'http-signature';
import config from '../config';
import { ILocalUser } from '../models/user'; import { ILocalUser } from '../models/user';
import { program } from '../argv'; import { program } from '../argv';
import handler from './processors'; import handler from './processors';
import { queueLogger } from './logger';
const enableQueue = config.redis != null && !program.disableQueue; const enableQueue = !program.disableQueue;
const queueAvailable = config.redis.isJust();
const queue = initializeQueue(); const queue = initializeQueue();
function initializeQueue() { function initializeQueue() {
if (enableQueue) { return config.redis.map(({ port, host, pass }) => {
return new Queue('misskey', { return new Queue('misskey', {
redis: { redis: {
port: config.redis.port, port: port,
host: config.redis.host, host: host,
password: config.redis.pass password: pass.getOrElse(null)
}, },
removeOnSuccess: true, removeOnSuccess: true,
@ -24,35 +27,48 @@ function initializeQueue() {
sendEvents: false, sendEvents: false,
storeJobs: false storeJobs: false
}); });
} else { }).getOrElse(null);
return null;
}
} }
export function createHttpJob(data: any) { export function deliver(user: ILocalUser, content: any, to: any) {
if (enableQueue) { if (content == null) return;
const data = {
type: 'deliver',
user,
content,
to
};
if (queueAvailable && !program.disableApQueue) {
return queue.createJob(data) return queue.createJob(data)
.retries(4) .retries(8)
.backoff('exponential', 16384) // 16s .backoff('exponential', 1000)
.save(); .save();
} else { } else {
return handler({ data }, () => {}); return handler({ data }, () => {});
} }
} }
export function deliver(user: ILocalUser, content: any, to: any) { export function processInbox(activity: any, signature: httpSignature.IParsedSignature) {
if (content == null) return; const data = {
type: 'processInbox',
activity: activity,
signature
};
createHttpJob({ if (queueAvailable && !program.disableApQueue) {
type: 'deliver', return queue.createJob(data)
user, .retries(3)
content, .backoff('exponential', 500)
to .save();
}); } else {
return handler({ data }, () => {});
}
} }
export function createExportNotesJob(user: ILocalUser) { export function createExportNotesJob(user: ILocalUser) {
if (!enableQueue) throw 'queue disabled'; if (!queueAvailable) throw 'queue unavailable';
return queue.createJob({ return queue.createJob({
type: 'exportNotes', type: 'exportNotes',
@ -61,8 +77,47 @@ export function createExportNotesJob(user: ILocalUser) {
.save(); .save();
} }
export default function() { export function createExportFollowingJob(user: ILocalUser) {
if (enableQueue) { if (!queueAvailable) throw 'queue unavailable';
queue.process(128, handler);
} return queue.createJob({
type: 'exportFollowing',
user: user
})
.save();
}
export function createExportMuteJob(user: ILocalUser) {
if (!queueAvailable) throw 'queue unavailable';
return queue.createJob({
type: 'exportMute',
user: user
})
.save();
}
export function createExportBlockingJob(user: ILocalUser) {
if (!queueAvailable) throw 'queue unavailable';
return queue.createJob({
type: 'exportBlocking',
user: user
})
.save();
}
export default function() {
if (queueAvailable && enableQueue) {
queue.process(128, handler);
queueLogger.succ('Processing started');
}
return queue;
}
export function destroy() {
queue.destroy().then(n => {
queueLogger.succ(`All job removed (${n} jobs)`);
});
} }

View File

@ -0,0 +1,89 @@
import * as bq from 'bee-queue';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
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';
const logger = queueLogger.createSubLogger('export-blocking');
export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
logger.info(`Exporting blocking of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
// 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}`);
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
let ended = false;
let cursor: any = null;
while (!ended) {
const blockings = await Blocking.find({
blockerId: user._id,
...(cursor ? { _id: { $gt: cursor } } : {})
}, {
limit: 100,
sort: {
_id: 1
}
});
if (blockings.length === 0) {
ended = true;
job.reportProgress(100);
break;
}
cursor = blockings[blockings.length - 1]._id;
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}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
exportedCount++;
}
const total = await Blocking.count({
blockerId: user._id,
});
job.reportProgress(exportedCount / total);
}
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
logger.succ(`Exported to: ${driveFile._id}`);
cleanup();
done();
}

View File

@ -0,0 +1,89 @@
import * as bq from 'bee-queue';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
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';
const logger = queueLogger.createSubLogger('export-following');
export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
logger.info(`Exporting following of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
// 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}`);
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
let ended = false;
let cursor: any = null;
while (!ended) {
const followings = await Following.find({
followerId: user._id,
...(cursor ? { _id: { $gt: cursor } } : {})
}, {
limit: 100,
sort: {
_id: 1
}
});
if (followings.length === 0) {
ended = true;
job.reportProgress(100);
break;
}
cursor = followings[followings.length - 1]._id;
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}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
exportedCount++;
}
const total = await Following.count({
followerId: user._id,
});
job.reportProgress(exportedCount / total);
}
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
logger.succ(`Exported to: ${driveFile._id}`);
cleanup();
done();
}

View File

@ -0,0 +1,89 @@
import * as bq from 'bee-queue';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
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';
const logger = queueLogger.createSubLogger('export-mute');
export async function exportMute(job: bq.Job, done: any): Promise<void> {
logger.info(`Exporting mute of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
// 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}`);
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
let ended = false;
let cursor: any = null;
while (!ended) {
const mutes = await Mute.find({
muterId: user._id,
...(cursor ? { _id: { $gt: cursor } } : {})
}, {
limit: 100,
sort: {
_id: 1
}
});
if (mutes.length === 0) {
ended = true;
job.reportProgress(100);
break;
}
cursor = mutes[mutes.length - 1]._id;
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}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
exportedCount++;
}
const total = await Mute.count({
muterId: user._id,
});
job.reportProgress(exportedCount / total);
}
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
logger.succ(`Exported to: ${driveFile._id}`);
cleanup();
done();
}

View File

@ -100,7 +100,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
stream.end(); stream.end();
logger.succ(`Exported to: ${path}`); logger.succ(`Exported to: ${path}`);
const fileName = dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json'; const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
const driveFile = await addFile(user, path, fileName); const driveFile = await addFile(user, path, fileName);
logger.succ(`Exported to: ${driveFile._id}`); logger.succ(`Exported to: ${driveFile._id}`);

View File

@ -1,12 +1,18 @@
import deliver from './http/deliver'; import deliver from './http/deliver';
import processInbox from './http/process-inbox'; import processInbox from './http/process-inbox';
import { exportNotes } from './export-notes'; import { exportNotes } from './export-notes';
import { exportFollowing } from './export-following';
import { exportMute } from './export-mute';
import { exportBlocking } from './export-blocking';
import { queueLogger } from '../logger'; import { queueLogger } from '../logger';
const handlers: any = { const handlers: any = {
deliver, deliver,
processInbox, processInbox,
exportNotes, exportNotes,
exportFollowing,
exportMute,
exportBlocking,
}; };
export default (job: any, done: any) => { export default (job: any, done: any) => {

View File

@ -43,11 +43,11 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
'Digest': `SHA-256=${hash}` 'Digest': `SHA-256=${hash}`
} }
}, res => { }, res => {
logger.info(`${url} --> ${res.statusCode}`);
if (res.statusCode >= 400) { if (res.statusCode >= 400) {
logger.warn(`${url} --> ${res.statusCode}`);
reject(res); reject(res);
} else { } else {
logger.succ(`${url} --> ${res.statusCode}`);
resolve(); resolve();
} }
}); });

View File

@ -19,16 +19,20 @@ export default class Resolver {
: value; : value;
switch (collection.type) { switch (collection.type) {
case 'Collection': case 'Collection': {
collection.objects = collection.items; collection.objects = collection.items;
break; break;
}
case 'OrderedCollection': case 'OrderedCollection': {
collection.objects = collection.orderedItems; collection.objects = collection.orderedItems;
break; break;
}
default: default: {
throw new Error(`unknown collection type: ${collection.type}`); logger.error(`unknown collection type: ${collection.type}`);
throw new Error(`unknown collection type: ${collection.type}`);
}
} }
return collection; return collection;
@ -36,6 +40,7 @@ export default class Resolver {
public async resolve(value: any): Promise<IObject> { public async resolve(value: any): Promise<IObject> {
if (value == null) { if (value == null) {
logger.error('resolvee is null (or undefined)');
throw new Error('resolvee is null (or undefined)'); throw new Error('resolvee is null (or undefined)');
} }
@ -44,6 +49,7 @@ export default class Resolver {
} }
if (this.history.has(value)) { if (this.history.has(value)) {
logger.error(`cannot resolve already resolved one`);
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
@ -51,7 +57,7 @@ export default class Resolver {
const object = await request({ const object = await request({
url: value, url: value,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: this.timeout, timeout: this.timeout,
headers: { headers: {
'User-Agent': config.user_agent, 'User-Agent': config.user_agent,
@ -59,6 +65,7 @@ export default class Resolver {
}, },
json: true json: true
}).catch(e => { }).catch(e => {
logger.error(`request error: ${e.message}`);
throw new Error(`request error: ${e.message}`); throw new Error(`request error: ${e.message}`);
}); });

View File

@ -3,7 +3,6 @@ import * as Router from 'koa-router';
import * as json from 'koa-json-body'; import * as json from 'koa-json-body';
import * as httpSignature from 'http-signature'; import * as httpSignature from 'http-signature';
import { createHttpJob } from '../queue';
import { renderActivity } from '../remote/activitypub/renderer'; import { renderActivity } from '../remote/activitypub/renderer';
import Note from '../models/note'; import Note from '../models/note';
import User, { isLocalUser, ILocalUser, IUser } from '../models/user'; import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
@ -17,6 +16,7 @@ import Followers from './activitypub/followers';
import Following from './activitypub/following'; import Following from './activitypub/following';
import Featured from './activitypub/featured'; import Featured from './activitypub/featured';
import renderQuestion from '../remote/activitypub/renderer/question'; import renderQuestion from '../remote/activitypub/renderer/question';
import { processInbox } from '../queue';
// Init router // Init router
const router = new Router(); const router = new Router();
@ -35,11 +35,7 @@ function inbox(ctx: Router.IRouterContext) {
return; return;
} }
createHttpJob({ processInbox(ctx.request.body, signature);
type: 'processInbox',
activity: ctx.request.body,
signature
});
ctx.status = 202; ctx.status = 202;
} }

View File

@ -0,0 +1,15 @@
import define from '../../../define';
import { destroy } from '../../../../../queue';
export const meta = {
requireCredential: true,
requireModerator: true,
params: {}
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
destroy();
res();
}));

View File

@ -0,0 +1,18 @@
import define from '../../define';
import { createExportBlockingJob } from '../../../../queue';
import ms = require('ms');
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 1,
},
};
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
createExportBlockingJob(user);
res();
}));

View File

@ -0,0 +1,18 @@
import define from '../../define';
import { createExportFollowingJob } from '../../../../queue';
import ms = require('ms');
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 1,
},
};
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
createExportFollowingJob(user);
res();
}));

View File

@ -0,0 +1,18 @@
import define from '../../define';
import { createExportMuteJob } from '../../../../queue';
import ms = require('ms');
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 1,
},
};
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
createExportMuteJob(user);
res();
}));

View File

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

View File

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

View File

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

View File

@ -96,13 +96,14 @@ app.use(router.routes());
app.use(mount(require('./web'))); app.use(mount(require('./web')));
function createServer() { function createServer() {
if (config.https) { if (config.https.isJust()) {
const certs: any = {}; const opts = {
for (const k of Object.keys(config.https)) { key: fs.readFileSync(config.https.get().key),
certs[k] = fs.readFileSync(config.https[k]); cert: fs.readFileSync(config.https.get().cert),
} ...config.https.get().ca.map<any>(path => ({ ca: fs.readFileSync(path) })).getOrElse({}),
certs['allowHTTP1'] = true; allowHTTP1: true
return http2.createSecureServer(certs, app.callback()) as https.Server; };
return http2.createSecureServer(opts, app.callback()) as https.Server;
} else { } else {
return http.createServer(app.callback()); return http.createServer(app.callback());
} }

View File

@ -42,7 +42,12 @@ export async function proxyMedia(ctx: Koa.BaseContext) {
ctx.body = image.data; ctx.body = image.data;
} catch (e) { } catch (e) {
serverLogger.error(e); serverLogger.error(e);
ctx.status = 500;
if (typeof e == 'number' && e >= 400 && e < 500) {
ctx.status = e;
} else {
ctx.status = 500;
}
} finally { } finally {
cleanup(); cleanup();
} }
@ -64,7 +69,7 @@ async function fetch(url: string, path: string) {
const req = request({ const req = request({
url: requestUrl, url: requestUrl,
proxy: config.proxy, proxy: config.proxy.getOrElse(null),
timeout: 10 * 1000, timeout: 10 * 1000,
headers: { headers: {
'User-Agent': config.user_agent 'User-Agent': config.user_agent

View File

@ -31,7 +31,9 @@ const app = new Koa();
app.use(views(__dirname + '/views', { app.use(views(__dirname + '/views', {
extension: 'pug', extension: 'pug',
options: { options: {
config config: {
url: config.url
}
} }
})); }));
@ -143,7 +145,11 @@ router.get('/@:user', async (ctx, next) => {
}); });
if (user != null) { if (user != null) {
await ctx.render('user', { user }); const meta = await fetchMeta();
await ctx.render('user', {
user,
instanceName: meta.name
});
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=180');
} else { } else {
// リモートユーザーなので // リモートユーザーなので
@ -179,9 +185,11 @@ router.get('/notes/:note', async ctx => {
if (note) { if (note) {
const _note = await packNote(note); const _note = await packNote(note);
const meta = await fetchMeta();
await ctx.render('note', { await ctx.render('note', {
note: _note, note: _note,
summary: getNoteSummary(_note) summary: getNoteSummary(_note),
instanceName: meta.name
}); });
if (['public', 'home'].includes(note.visibility)) { if (['public', 'home'].includes(note.visibility)) {

View File

@ -6,7 +6,7 @@ block vars
- const url = `${config.url}/notes/${note.id}`; - const url = `${config.url}/notes/${note.id}`;
block title block title
= `${title} | ${config.name}` = `${title} | ${instanceName}`
block desc block desc
meta(name='description' content= summary) meta(name='description' content= summary)

View File

@ -6,7 +6,7 @@ block vars
- const img = user.avatarUrl || null; - const img = user.avatarUrl || null;
block title block title
= `${title} | ${config.name}` = `${title} | ${instanceName}`
block desc block desc
meta(name='description' content= user.description) meta(name='description' content= user.description)

View File

@ -50,8 +50,9 @@ async function save(path: string, name: string, type: string, hash: string, size
if (type === 'image/webp') ext = '.webp'; if (type === 'image/webp') ext = '.webp';
} }
const baseUrl = config.drive.baseUrl 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 }`; `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`
);
// for original // for original
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;

View File

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

View File

@ -500,7 +500,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
} }
function index(note: INote) { function index(note: INote) {
if (note.text == null || config.elasticsearch == null) return; if (note.text == null || !config.elasticsearch.isJust()) return;
es.index({ es.index({
index: 'misskey', index: 'misskey',

View File

@ -37,8 +37,9 @@ async function job(file: IDriveFile): Promise<any> {
const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`; const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`; const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
const baseUrl = config.drive.baseUrl 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 }`; `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`
);
const bucket = await getDriveFileBucket(); const bucket = await getDriveFileBucket();
const readable = bucket.openDownloadStream(file._id); const readable = bucket.openDownloadStream(file._id);

View File

@ -611,6 +611,14 @@ describe('MFM', () => {
text('(#123)'), text('(#123)'),
]); ]);
}); });
it('ignore slash', () => {
const tokens = parse('#foo/bar');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'foo' }),
text('/bar'),
]);
});
}); });
describe('quote', () => { describe('quote', () => {

28
test/prelude/maybe.ts Normal file
View File

@ -0,0 +1,28 @@
/*
* Tests of Maybe
*
* How to run the tests:
* > mocha test/prelude/maybe.ts --require ts-node/register
*
* To specify test:
* > mocha test/prelude/maybe.ts --require ts-node/register -g 'test name'
*/
import * as assert from 'assert';
import { just, nothing } from '../../src/prelude/maybe';
describe('just', () => {
it('has a value', () => {
assert.deepStrictEqual(just(3).isJust(), true);
});
it('has the inverse called get', () => {
assert.deepStrictEqual(just(3).get(), 3);
});
});
describe('nothing', () => {
it('has no value', () => {
assert.deepStrictEqual(nothing().isJust(), false);
});
});