Compare commits

...

87 Commits

Author SHA1 Message Date
syuilo
bb6ede2b8f 10.92.4 2019-03-11 13:59:27 +09:00
syuilo
822400a1ba New Crowdin translations (#4461)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Czech)
2019-03-11 13:57:21 +09:00
syuilo
e3e08843f1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-11 09:59:14 +09:00
syuilo
ce0d4f77fa リストのエクスポートをできるように
#4259
2019-03-11 09:59:07 +09:00
Acid Chicken (硫酸鶏)
94fdb4e974 Update README.md [AUTOGEN] (#4467) 2019-03-11 08:13:02 +09:00
MeiMei
4d425fc8a4 Use proxy instead of weserv for url-preview images (#4466) 2019-03-11 01:03:09 +09:00
MeiMei
c6cdfa2f5a Ignore 4xx references in AP (#4463)
* Ignore 4xx references

* remove unnecessary comment
2019-03-10 22:27:25 +09:00
syuilo
0fff2e4f16 Remove debug code 2019-03-10 19:20:25 +09:00
syuilo
80a2172715 Resolve #4462 2019-03-10 19:16:33 +09:00
syuilo
5a0a297634 Improve redis config 2019-03-09 23:44:54 +09:00
syuilo
948a133b7b Fix log 2019-03-09 18:15:44 +09:00
syuilo
2ee826c958 10.92.3 2019-03-09 10:21:45 +09:00
syuilo
539409faf8 Better logs 2019-03-09 10:18:59 +09:00
syuilo
606e46e4d7 Make info 2019-03-09 10:10:24 +09:00
syuilo
a179cfd69a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-09 10:09:12 +09:00
syuilo
d8379253d4 Rename 2019-03-09 10:09:04 +09:00
MeiMei
c3344fbd68 To retry AP deliver queue (#4457) 2019-03-09 08:57:55 +09:00
syuilo
4cebd6e84a Increase job attempts limit a little 2019-03-08 21:43:17 +09:00
syuilo
90fbf9dbb0 Add missing semicolon 2019-03-08 21:38:23 +09:00
syuilo
d365b9f634 Fix bug 2019-03-08 21:30:12 +09:00
syuilo
a2f06acaa4 10.92.2 2019-03-08 21:00:35 +09:00
syuilo
8c90cbcbfb Disable lifo 2019-03-08 20:58:01 +09:00
syuilo
a4a47772dc New Crowdin translations (#4447)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

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

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

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

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

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Polish)
2019-03-08 20:55:36 +09:00
syuilo
5dde1f4602 Update README.md 2019-03-08 20:52:09 +09:00
syuilo
9dc0909eeb Add index 2019-03-08 20:36:42 +09:00
syuilo
0ed2592e41 Resolve #4453 2019-03-08 20:07:29 +09:00
syuilo
76cff98220 Fix bug 2019-03-08 19:45:01 +09:00
syuilo
60604b6f51 🎨 2019-03-08 15:34:34 +09:00
syuilo
f410b7aecb 🎨 2019-03-08 15:27:06 +09:00
syuilo
1a61f2cee9 自動更新 2019-03-08 13:10:38 +09:00
syuilo
78a8293520 ジョブの数を一覧できるように 2019-03-08 13:03:38 +09:00
syuilo
03cfb4fc8d 🎨 2019-03-08 13:03:17 +09:00
syuilo
144345a359 Enable lifo job 2019-03-08 05:39:59 +09:00
syuilo
fd2c01515e Increase job concurrency 2019-03-08 05:23:13 +09:00
syuilo
219570e08b Update job handlers 2019-03-08 05:22:14 +09:00
syuilo
69df556ff5 Resolve #4448 2019-03-08 04:55:55 +09:00
syuilo
5f4a52574f Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 23:43:56 +09:00
syuilo
5a1f6c5839 10.92.1 2019-03-07 23:43:42 +09:00
syuilo
91d0342fe8 New Crowdin translations (#4445)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)
2019-03-07 23:41:13 +09:00
syuilo
8cc236daf8 Clean jobs 2019-03-07 23:36:08 +09:00
syuilo
d283ec69f7 Refactor 2019-03-07 23:27:38 +09:00
syuilo
d1aea7596c Fix import 2019-03-07 23:14:01 +09:00
syuilo
c934987b14 Resolve #4444 2019-03-07 23:07:21 +09:00
syuilo
00c9f4a2e5 Update issue templates 2019-03-07 21:41:39 +09:00
syuilo
6605c1d07f New Crowdin translations (#4431)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)
2019-03-07 21:19:53 +09:00
MeiMei
7325d66c52 Implement Update Question (#4435)
* Update remote votes count

* save updatedAt

* deliver Update

* use renderNote

* use id

* fix typeof
2019-03-07 21:19:32 +09:00
Acid Chicken (硫酸鶏)
a485061e22 Update README.md [AUTOGEN] (#4441) 2019-03-07 20:30:41 +09:00
Acid Chicken (硫酸鶏)
1f63f50343 Create robots.txt (#4439)
* Create robots.txt

* Update index.ts
2019-03-07 20:11:04 +09:00
syuilo
cd3170dabd Update PULL_REQUEST_TEMPLATE.md 2019-03-07 20:06:36 +09:00
syuilo
841cedc5f8 Update issue templates 2019-03-07 19:56:02 +09:00
dependabot[bot]
7f4882734d Update @types/speakeasy requirement from 2.0.3 to 2.0.4 (#4369)
Updates the requirements on [@types/speakeasy](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/speakeasy) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/speakeasy)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:41:08 +09:00
dependabot[bot]
e7d647d412 Update eslint requirement from 5.12.0 to 5.15.0 (#4394)
Updates the requirements on [eslint](https://github.com/eslint/eslint) to permit the latest version.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v5.12.0...v5.15.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:40:55 +09:00
dependabot[bot]
913d14a58a Update @types/node requirement from 10.12.24 to 11.10.4 (#4396)
Updates the requirements on [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:40:41 +09:00
dependabot[bot]
909272ec3d Update @types/koa-router requirement from 7.0.39 to 7.0.40 (#4418)
Updates the requirements on [@types/koa-router](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/koa-router) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/koa-router)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:40:22 +09:00
dependabot[bot]
7af40ffbbe Update webpack-cli requirement from 3.2.1 to 3.2.3 (#4422)
Updates the requirements on [webpack-cli](https://github.com/webpack/webpack-cli) to permit the latest version.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v.3.2.1...V.3.2.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:40:09 +09:00
dependabot[bot]
9df79a3ec9 Update file-type requirement from 10.7.1 to 10.9.0 (#4423)
Updates the requirements on [file-type](https://github.com/sindresorhus/file-type) to permit the latest version.
- [Release notes](https://github.com/sindresorhus/file-type/releases)
- [Commits](https://github.com/sindresorhus/file-type/compare/v10.7.1...v10.9.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:39:52 +09:00
dependabot[bot]
4f2eee06aa Update bootstrap-vue requirement from 2.0.0-rc.11 to 2.0.0-rc.13 (#4353)
Updates the requirements on [bootstrap-vue](https://github.com/bootstrap-vue/bootstrap-vue) to permit the latest version.
- [Release notes](https://github.com/bootstrap-vue/bootstrap-vue/releases)
- [Changelog](https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/bootstrap-vue/bootstrap-vue/compare/v2.0.0-rc.11...v2.0.0-rc.13)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-07 19:39:39 +09:00
syuilo
1b9cf76008 Update issue templates 2019-03-07 18:16:49 +09:00
syuilo
d035a43ed6 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 16:50:41 +09:00
syuilo
95ee9a6e09 Update issue templates 2019-03-07 16:50:05 +09:00
Acid Chicken (硫酸鶏)
02a63cdcb3 Update index.js 2019-03-07 16:43:16 +09:00
syuilo
f02125dd47 Resolve #4437 2019-03-07 13:03:46 +09:00
syuilo
c11e813146 Fix 2019-03-07 09:35:23 +09:00
syuilo
a365849048 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 09:31:07 +09:00
syuilo
a493c9f769 🎨 2019-03-07 09:30:44 +09:00
Acid Chicken (硫酸鶏)
a13f522b2a Update README.md [AUTOGEN] (#4436) 2019-03-07 09:18:57 +09:00
syuilo
1ed70b2e2c 10.92.0 2019-03-06 23:37:53 +09:00
syuilo
86d5a599b7 New Crowdin translations (#4414)
* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (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 (Czech)
2019-03-06 23:30:39 +09:00
Acid Chicken (硫酸鶏)
c226fc8d63 Make building locales better (#4427) 2019-03-06 23:28:50 +09:00
syuilo
bbf4e1c413 Add fallback for broken db 2019-03-06 23:23:50 +09:00
syuilo
a24a20a83d Clean up 2019-03-06 23:19:15 +09:00
Acid Chicken (硫酸鶏)
725600da8f Enhance poll (#4409)
* Start working

* WIP: Enhance poll

* Fix bug

* Use `name` in voting note
refs: https://github.com/syuilo/misskey/issues/4407#issuecomment-469057296

* Fix style

* Refactor
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com>

* WIP: Update poll editor

* Fix bug

* Fix bug
refs: https://github.com/syuilo/misskey/pull/4409#discussion_r

* Fix typo

* Better design

* Beautify poll editor

* Fix UI

* Fix bug
refs: https://github.com/syuilo/misskey/pull/4409#discussion_r262217524

* Add debug logging

* Fix bug

* Log deliver

* fix vote

* Update ap/show
refs: https://github.com/syuilo/misskey/pull/4409#issuecomment-469652386

* Update poll view

* Maybe done

* Add tests

* Fix path

* Fix test

* Fix test

* Fix test

* Fix expired check on AP

* Update note.ts

* Squashed commit of the following:

commit d9a4beabf851893b8992a0f4568265eb9d4f0b8e
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 05:16:14 2019 +0900

    tune

commit 83ff421a6e978243f80ba9ec820189bc897e6e3b
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 05:01:14 2019 +0900

    fallback

commit 0b566af973b115ade9e75ea4b8094ee2b329dabc
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 04:40:12 2019 +0900

    Note

commit cc0296dd6127580ac584c40398db3f762a311f8b
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 04:33:58 2019 +0900

    createで送る

* Squashed commit of the following:

commit ae696b1ed12568b27c27367ac5a77035c97c9a1f
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 06:11:17 2019 +0900

    fix

commit b735e354e7a9e64534c4f17d04ecbc65fb735c21
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 06:08:33 2019 +0900

    messge

commit d9a4beabf851893b8992a0f4568265eb9d4f0b8e
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 05:16:14 2019 +0900

    tune

commit 83ff421a6e978243f80ba9ec820189bc897e6e3b
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 05:01:14 2019 +0900

    fallback

commit 0b566af973b115ade9e75ea4b8094ee2b329dabc
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 04:40:12 2019 +0900

    Note

commit cc0296dd6127580ac584c40398db3f762a311f8b
Author: mei23 <m@m544.net>
Date:   Wed Mar 6 04:33:58 2019 +0900

    createで送る

* Fix typo

* Update vote.ts

* Update vote.ts

* Update poll-editor.vue

* Update tslint.json

* Fix layout

* Add note

* Fix bug

* Rename text key

* 投票するときに投稿として扱わないように (#4425)

* wip

* 形式をMastodonと合わせた

* Bye something

* Use - instead of ~

* Redundancy

* Yes!

* Refactor

* Use moment instead of Date

* Fix indent

* Refactor

if (votes.length)
は必要なさそう

* Clean up

* Bye Date

* Clean

* Fix timer is not displayed

* Fix リモートから無期限pollにvoteできない

* Fix vote actor
2019-03-06 22:55:47 +09:00
syuilo
f74a32ed9b [Client] Fix bug 2019-03-06 20:17:59 +09:00
Acid Chicken (硫酸鶏)
e08e72dd10 Use meta url instead of constant url 2019-03-06 12:31:58 +09:00
Acid Chicken (硫酸鶏)
ce02e1e528 Bye on-build-webpack (#4404)
* Update webpack.config.ts

* Update package.json
2019-03-06 09:26:22 +09:00
syuilo
0b27d8a717 Fix #4421 2019-03-06 09:24:16 +09:00
syuilo
2782e7d26f [MFM] Improve hashtag parsing 2019-03-05 22:18:29 +09:00
syuilo
2c83a05e80 [Client] Better transition 2019-03-05 20:45:19 +09:00
syuilo
467f68502a Refactor 2019-03-05 20:32:39 +09:00
syuilo
d95b0dee6b Fix #4416 2019-03-05 08:27:50 +09:00
syuilo
a1f3323fa5 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-05 08:24:26 +09:00
syuilo
494796a7f0 Fix #4415 2019-03-05 08:24:19 +09:00
MeiMei
94f2c20d35 Fix #4413 (#4417) 2019-03-05 07:56:23 +09:00
Acid Chicken (硫酸鶏)
c1deb9438d Update README.md [AUTOGEN] (#4411) 2019-03-04 19:27:53 +09:00
syuilo
ea86527c66 Add indexes 2019-03-04 19:26:05 +09:00
syuilo
d1a18fe266 Resolve #3220 2019-03-04 14:32:36 +09:00
syuilo
737064da82 Better log 2019-03-04 14:02:42 +09:00
114 changed files with 2535 additions and 635 deletions

View File

@@ -1,30 +1,30 @@
---
name: Bug Report
name: 🐛 Bug Report
about: Create a report to help us improve
title: ''
labels: bug
labels: ⚠️bug?
assignees: ''
---
# Summary
## Summary
<!-- Tell us what the bug is -->
# Expected Behavior
## Expected Behavior
<!--- Tell us what should happen -->
# Actual Behavior
## Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
# Steps to Reproduce
## Steps to Reproduce
1.
2.
3.
# Environment
## Environment
<!-- Tell us where on the platform it happens -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# Summary
## Summary
<!--
-

View File

@@ -1,6 +1,44 @@
ChangeLog
=========
10.92.4
----------
* リストのエクスポートをできるように
* ジョブキューウィジェットを追加
* URLプレビューのサムネイルが表示されないことがある問題を修正
10.92.3
----------
* 管理画面の各種ジョブ数がおかしい問題を修正
* ジョブキューの動作を調整
10.92.2
----------
* 管理画面で各種ジョブ数を一覧できるように
* ジョブキューの動作を修正
* notes/children が遅い問題を修正
10.92.1
----------
* アンケートの結果をリモートと同期するように
* ジョブキューを有効に
* 投稿の返信一覧に引用Renoteも含めるように
* robots.txt追加
* デザインの調整
10.92.0
----------
* Mastodonのアンケートに対応
* 複数回答できるアンケートを作成できるように
* アンケートに期限を設定できるように
* 絵文字ピッカーを改良
* ハッシュタグの判定を改善
* デッキのタグTLで別のタグをクリックしてもTLが変わらない問題を修正
* ユーザーサジェストで表示名が変わらない問題を修正
* UIのバグ修正
* デザインの調整
* など
10.91.2
----------
* 10.91.1 で追加した依存関係にXSS脆弱性があったので他のパッケージに差し替え

View File

@@ -1,4 +1,4 @@
<img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/>
<a href="https://ai.misskey.xyz/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
[![Misskey](/assets/title.png)](https://misskey.xyz/)
================================================================
@@ -103,7 +103,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/3?token-time=2145916800&token-hash=c8HeVqLtmdgH-gSBJg8i10gmOcwllM87MDHeznl3el0%3D" alt="Melilot" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=LtV2lRi3L2jOWMLwccr9qWYfPrFlzIo2jYZHKzHEb6k%3D" alt="Xeltica" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td>
@@ -124,6 +124,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1?token-time=2145916800&token-hash=95p8VdGX45E8BitZR_eOcDlqCjumjzNLBPQJrJdeCpI%3D" alt="takimura" 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/16900731/935a10339daa4ede8e555903a0707060/1?token-time=2145916800&token-hash=3CrpqH-XtKs_NoIlSsTyVs8wCzP1WFCsG2xwps1IJq0%3D" alt="Atsuko Tominaga" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
@@ -133,10 +134,12 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td>
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
</tr></table>
<table><tr>
<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/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="@Hekovic@gyutte.site" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td>
@@ -144,13 +147,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
</tr><tr>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/user?u=18072312">@Hekovic@gyutte.site</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Fri, 01 Mar 2019 23:59:07 UTC
**Last updated:** Sun, 10 Mar 2019 22:17:05 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

4
assets/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
user-agent: *
allow: /
# todo: sitemap

View File

@@ -5,7 +5,6 @@
import * as gulp from 'gulp';
import * as gutil from 'gulp-util';
import * as ts from 'gulp-typescript';
const yaml = require('gulp-yaml');
const sourcemaps = require('gulp-sourcemaps');
import tslint from 'gulp-tslint';
const cssnano = require('gulp-cssnano');
@@ -126,12 +125,6 @@ gulp.task('copy:client', () =>
.pipe(gulp.dest('./built/client/assets/'))
);
gulp.task('locales', () =>
gulp.src('./locales/*.yml')
.pipe(yaml({ schema: 'DEFAULT_SAFE_SCHEMA' }))
.pipe(gulp.dest('./built/client/assets/locales/'))
);
gulp.task('doc', () =>
gulp.src('./src/docs/**/*.styl')
.pipe(stylus())
@@ -149,7 +142,6 @@ gulp.task('build', gulp.parallel(
'build:ts',
'build:copy',
'build:client',
'locales',
'doc'
));

View File

@@ -93,7 +93,7 @@ common:
love: "Super"
laugh: "Smích"
hmm: "Hmm...?"
surprise: "Páni"
surprise: "Překvapení"
congrats: "Gratuluji!"
angry: "Naštvaný"
confused: "Zmatený"
@@ -101,14 +101,14 @@ common:
pudding: "Pudink"
note-visibility:
public: "Veřejná"
home: "Domovský"
home: "Domovská"
home-desc: "Poslat pouze na domovskou časovou osu"
followers: "Pro sledující"
followers-desc: "Poslat pouze sledujícím"
specified: "Přímý"
specified: "Přímá"
specified-desc: "Poslat pouze zmíněným uživatelům"
local-public: "Veřejný (pouze místní)"
local-home: "Domovský (pouze místní)"
local-public: "Veřejná (pouze místní)"
local-home: "Domovská (pouze místní)"
local-followers: "Pro sledující (pouze místní)"
note-placeholders:
a: "Co právě děláte?"
@@ -122,7 +122,8 @@ common:
profile: "Profil"
notification: "Oznámení"
apps: "Aplikace"
tags: "Tagy"
tags: "Hashtagy"
mute-and-block: "Ztlumit/blokovat"
blocking: "Blokování"
security: "Zabezpečení"
signin: "Historie přihlášení"
@@ -131,97 +132,182 @@ common:
appearance: "Vzhled"
behavior: "Chování"
fetch-on-scroll: "Nekonečné rolování"
note-visibility: "Viditelnost statusu"
default-note-visibility: "Výchozí viditelnost statusu"
fetch-on-scroll-desc: "Pokud budete rolovat dolů po stránce, automaticky bude načten další obsah."
note-visibility: "Viditelnost příspěvku"
default-note-visibility: "Výchozí viditelnost příspěvku"
remember-note-visibility: "Zapamatovat viditelnost příspěvků"
web-search-engine: "Webové vyhledávače"
web-search-engine-desc: "Například: https://www.google.com/?#q={{query}}"
keep-cw: "Zachovat varování o obsahu"
keep-cw-desc: "Při odpovědi na příspěvek bude varování o obsahu nastaveno stejně jako původní příspěvek."
i-like-sushi: "Mam radši sushi (než puding)"
show-reversi-board-labels: "Zobrazit označení řad a sloupců v Reversi"
use-avatar-reversi-stones: "Použít avatar jako figurku v Reversi"
disable-animated-mfm: "Vypnout pohyblivé texty v příspěvku"
disable-showing-animated-images: "Nepřehrávat animované obrázky"
suggest-recent-hashtags: "Navrhovat nedávné hashtagy v rámci psacího pole"
always-show-nsfw: "Vždycky ukázat NSFW obsah"
always-mark-nsfw: "Označovat všechny příspěvky za delikátní"
show-full-acct: "Zaradit hostovací server jako součast přezdívky"
show-via: "zobrazit přes"
reduce-motion: "Snížit pohyb v rozhraní"
this-setting-is-this-device-only: "Pouze pro toto zařízení"
use-os-default-emojis: "Použít výchozí emoji systému"
line-width: "Hrubka línie"
line-width-thin: "Úzka"
line-width-normal: "Běžná"
line-width-thick: "Tlustá"
font-size: "Velikost písma"
font-size-x-small: "Malé"
font-size-small: "Dost malé"
font-size-medium: "Průměrné"
font-size-large: "Dost velké"
font-size-x-large: "Velké"
deck-column-align: "Zarovnání sloupců v Decku"
deck-column-align-center: "Na střed"
deck-column-align-left: "Vlevo"
deck-column-width-wide: "široké"
wallpaper: "Pozadí"
deck-column-align-flexible: "Flexibilní"
deck-column-width: "Šířka sloupců v Decku"
deck-column-width-narrow: "Úzké"
deck-column-width-narrower: "Poněkud úzké"
deck-column-width-normal: "Normální"
deck-column-width-wider: "Poněkud široké"
deck-column-width-wide: "Široké"
use-shadow: "Používat v rozhraní stíny"
rounded-corners: "Zakulatit rohy v rozhraní"
circle-icons: "Používat kulaté ikony"
contrasted-acct: "Přidat uživatelskému účtu kontrast"
wallpaper: "Obrázek na pozadí"
choose-wallpaper: "Zvolit pozadí"
delete-wallpaper: "Odstranit pozadí"
post-form-on-timeline: "Zobrazit formulář pro nové příspěvky nad časovou osou"
show-clock-on-header: "Zobrazit hodiny v pravém horním rohu"
show-reply-target: "Zobrazit cíl odpovědi"
timeline: "Časová osa"
show-my-renotes: "Zobrazit moje renoty v časové ose"
show-renoted-my-notes: "Zobrazit renoty vašich vlastních příspěvků v časové ose"
show-local-renotes: "Zobrazit renoty místních příspěvků v časové ose"
remain-deleted-note: "I nadále zobrazovat odstraněné příspěvky"
sound: "Zvuk"
enable-sounds: "Povolit zvuk"
update: "Misskey aktualizace"
enable-sounds-desc: "Přehrát zvuk, například při odeslání nebo přijetí příspěvku, či zprávy. Toto nastavení je uloženo v prohlížeči."
volume: "Hlasitost"
test: "Test"
update: "Aktualizace Misskey"
version: "Verze:"
latest-version: "Nejnovější verze:"
advanced-settings: "Pokročilé nastavení"
navbar-position: "Pozice navigace"
update-checking: "Kontroluji aktualizace"
do-update: "Zkontrolovat aktualizace"
update-settings: "Pokročilá nastavení"
no-updates: "Nejsou dostupné žádné aktualizace"
no-updates-desc: "Váš server Misskey je aktuální."
update-available: "Je dostupná nová verze"
update-available-desc: "Aktualizace budou aplikovány po znovunačtení stránky."
advanced-settings: "Pokročilá nastavení"
debug-mode: "Povolit režim ladění"
debug-mode-desc: "Toto nastavení je uloženo v prohlížeči."
navbar-position: "Poloha navigačního panelu"
navbar-position-top: "Nahoře"
navbar-position-left: "Vlevo"
navbar-position-right: "Vpravo"
i-am-under-limited-internet: "Mam omezený (pomalý) internet"
post-style: "Styl zobrazení poznámek"
post-style-standard: "Standardní"
post-style-smart: "Chytrý"
notification-position: "Poloha oznámení"
notification-position-bottom: "Dole"
notification-position-top: "Nahoře"
load-raw-images: "Zobrazit obrázky v originální kvalitě"
search: "Vyhledávání"
disable-via-mobile: "Neoznačovat příspěvky jako „z mobilu“"
load-raw-images: "Zobrazovat obrázky v původní kvalitě"
load-remote-media: "Zobrazovat média ze vzdáleného serveru"
search: "Hledání"
delete: "Odstranit"
loading: "Nahrávám..."
loading: "Načítám..."
ok: "OK"
cancel: "Zrušit"
update-available-title: "Aktualizace k dispozici"
update-available: "Nová verze Misskey je k dispozici({newer}, Vaše verze je {current}). Načtěte znovu stránku pro aktivování nové verze."
update-available: "Je k dispozici nová verze Misskey ({newer},vaše verze je {current}). Pro aplikování nové verze znovunačtěte stránku."
verified-user: "Ověřené účty"
hide-password: "Skrýt heslo"
show-password: "Zobrazit heslo"
do-not-use-in-production: "Tohle je vývojářský build. Nepoužívejte v produkci."
user-suspended: "Tomuto uživateli byl pozastaven účet."
is-remote-user: "Informace o tomto uživateli nemusí být kompletní."
is-remote-post: "Obsah tohoto příspěvku je zrcadlen."
view-on-remote: "Pro kompletnost jej zobrazte vzdáleně."
renoted-by: "{user} renotoval/a"
no-notes: "Bez poznámek"
turn-on-darkmode: "Přepnout na tmavý režim"
turn-off-darkmode: "Světlý režim"
error:
title: "Něco se stalo :("
retry: "Zkusit znovu"
reversi:
drawn: "Remíza"
my-turn: "Váš tah"
opponent-turn: "Je řada na protivníkovi"
turn-of: "{name} je na tahu"
won: "{name} vyhrál"
past-turn-of: "{name} byl/a na tahu"
won: "{name} vyhrál/a"
black: "Černá"
white: "Bílá"
total: "Celkem"
this-turn: "{count}. kolo"
widgets:
analog-clock: "Analogové hodiny"
profile: "Profil"
calendar: "Kalendář"
timemachine: "Kalendář (Stroj času)"
activity: "Aktivita"
rss: "RSS čtečka"
memo: "Poznámky"
memo: "Rychlé poznámky"
trends: "Trendy"
photo-stream: "Proud fotek"
posts-monitor: "Grafy příspěvků"
slideshow: "Prezentace"
version: "Verze"
broadcast: "Rozhlas"
notifications: "Oznámení"
users: "Doporučení uživatelé"
polls: "Ankety"
post-form: "Formulář pro psaní"
server: "Informace o serveru"
nav: "Navigace"
tips: "Tipy"
hashtags: "Hashtagy"
dev: "Nepodařilo se vytvořit aplikace. Prosím zkuste to znovu."
ai-chan-kawaii: "Ai-chan kawaii!"
you: "Vy"
auth/views/form.vue:
share-access: "Chcete dovolit <i>{name}</i> přístup k Vašemu účtu?"
share-access: "Chcete dovolit aplikaci <i>{name}</i> přístup k vašemu účtu?"
permission-ask: "Tato aplikace vyžaduje následující oprávnění:"
account-read: "Zobrazit informace účtu"
note-write: "Odeslat."
following-write: "Sledovat a přestat sledovat"
drive-read: "Přečíst váš Disk"
notification-read: "Sledovat oznámení."
notification-write: "Zpravovat notifikace."
cancel: "Zrušit"
accept: "Povolit přístup"
auth/views/index.vue:
loading: "Nahrávám..."
loading: "Načítám..."
already-authorized: "Tato aplikace byla již autorizována."
error: "Taková relace neexistuje."
sign-in: "Prosím přihlaste se."
common/views/pages/explore.vue:
verified-users: "Ověřené účty"
popular-users: "Populární uživatelé"
recently-updated-users: "Nedávno aktívni uživatelé"
recently-registered-users: "Nedávno registrovaní uživatelé"
popular-tags: "Populární tagy"
federated: "Z fediversu"
federated: "Z fediverse"
common/views/components/url-preview.vue:
enable-player: "Otevřít v přehrávači"
common/views/components/user-list.vue:
no-users: "Žádní uživatelé"
common/views/components/games/reversi/reversi.vue:
matching:
waiting-for: "Čeká se na {}"
cancel: "Zrušit"
common/views/components/games/reversi/reversi.game.vue:
surrender: "Vzdát se"
@@ -235,14 +321,21 @@ common/views/components/games/reversi/reversi.index.vue:
my-games: "Moje hra"
all-games: "Všechny hry"
enter-username: "Zadejte své uživatelské jméno"
game-state:
ended: "Ukončené"
playing: "Probíhají"
common/views/components/games/reversi/reversi.room.vue:
settings-of-the-game: "Nastavení hry"
choose-map: "Vybrat mapu"
random: "Náhodně"
black-or-white: "Černé/bílé"
black-is: "Černá je {}"
rules: "Pravidla"
looped-map: "Zacyklená mapa"
settings-of-the-bot: "Nastavení Botu"
this-game-is-started-soon: "Hra začne za pár vteřin"
waiting-for-other: "Čeká se na protivníka"
waiting-for-me: "Čeká se na Vás"
waiting-for-both: "Připravuji"
cancel: "Zrušit"
ready: "Připraveno"
@@ -255,7 +348,22 @@ common/views/components/connect-failed.troubleshooter.vue:
checking-network: "Prověřit síťové připojení"
internet: "Připojení k internetu"
checking-internet: "Ověřuji připojení k internetu."
server: "Připojení k serveru"
no-network-desc: "Ujistěte se že jste připojeni k Internetu."
no-internet: "Nejste připojeni k internetu"
no-internet-desc: "Jste připojen k síti, ale zdá se že stále chybí připojení k Internetu. Prosím zkontrolujte Vaše připojení k Internetu."
common/views/components/media-banner.vue:
click-to-show: "Klikněte pro zobrazení"
common/views/components/theme.vue:
light-theme: "Šablona pro použití ve světlém vzhledu"
dark-theme: "Šablona pro použití v tmavém vzhledu"
light-themes: "Světlý vzhled"
dark-themes: "Tmavý vzhled"
install-a-theme: "Nainstalovat šablonu"
theme-code: "Kód šablony"
install: "Nainstalovat"
installed: "\"{}\" byl nainstalován"
create-a-theme: "Vytvořit motiv"
base-theme: "Základní vzhled"
find-more-theme: "Najít další vzhledy"
theme-name: "Jméno vzhledu"
@@ -289,6 +397,7 @@ common/views/components/messaging-room.vue:
only-one-file-attached: "Jenom JEDEN soubor může být přiložen ke zprávě."
common/views/components/messaging-room.form.vue:
send: "Odeslat"
attach-from-local: "Přiložit soubory z Vašeho zařízení"
only-one-file-attached: "Jenom JEDEN soubor může být přiložen ke zprávě."
common/views/components/messaging-room.message.vue:
is-read: "Přečtené"
@@ -301,13 +410,41 @@ common/views/components/nav.vue:
donors: "Dárci"
repository: "Úložiště"
develop: "Vývojáři"
feedback: "Zpětná vazba"
common/views/components/note-menu.vue:
mention: "Zmínění"
copy-content: "Zkopírovat obsah"
copy-link: "Zkopírovat odkaz"
favorite: "Přidat do oblíbených"
unfavorite: "Odebrat z oblízených"
watch: "Sledovat"
unwatch: "Přestat sledovat"
delete: "Odstranit"
delete-confirm: "Opravdu chcete smazat tento příspěvek?"
remote: "Ukázat originální poznámku"
common/views/components/user-menu.vue:
mention: "Zmínění"
mute: "Umlčet"
block: "Blokováno"
unmute: "Zrušit umlčení"
block: "Blokován"
unblock: "Odblokovat"
push-to-list: "Přidat do seznamu"
select-list: "Vyberte seznam"
report-abuse-reported: "Problém byl nahlášen administrátorovi. Děkujeme za Vaší kooperaci."
common/views/components/poll.vue:
vote-count: "{} hlasů"
vote: "Hlasovat"
show-result: "Podívat se na výsledky"
voted: "Už jste hlasovaly"
remaining-days: "zbývá {d} dnů, {h} hodin"
remaining-hours: "zbývá {h} hodin, a {m} minut"
remaining-minutes: "zbývá {m} minut, a {s} sekund"
remaining-seconds: "zbývá {s} sekund"
common/views/components/poll-editor.vue:
no-only-one-choice: "Musíte vybrat alespoň dvě možnosti"
day: "Ne"
common/views/components/emoji-picker.vue:
custom-emoji: "Emoji"
people: "Lidé"
animals-and-nature: "Zvířata a příroda"
food-and-drink: "Jídlo a pití"
@@ -360,20 +497,53 @@ common/views/components/notification-settings.vue:
mark-as-read-all-notifications: "Označit všechna oznámení za přečtená"
mark-as-read-all-unread-notes: "Označit všechny příspěvky za přečtené"
mark-as-read-all-talk-messages: "Označit všechny zprávy za přečtené"
common/views/components/integration-settings.vue:
connect: "Připojit"
disconnect: "Odpojit"
common/views/components/github-setting.vue:
description: "Jakmile spojíte Váš GitHub účet s Vaším Misskey účtem, uvidíte informace o Vašem GitHub účtu na Vašem profilu a budete se moci přihlásit skrze GitHub."
connected-to: "Je připojen k tomuto GitHub účtu"
detail: "Více…"
reconnect: "Znovu připojit"
connect: "Připojit Váš GitHub účet"
disconnect: "Odpojit"
common/views/components/discord-setting.vue:
description: "Jakmile spojíte Váš Discord účet s Vaším Misskey účtem, uvidíte informace o Vašem Discord účtu na Vašem profilu a budete se moci přihlásit skrze Discord."
connected-to: "Je připojen k tomuto Discord účtu"
detail: "Více…"
reconnect: "Znovu připojit"
connect: "Připojit Váš Discord účet"
disconnect: "Odpojit"
common/views/components/uploader.vue:
waiting: "Čekáme"
common/views/components/visibility-chooser.vue:
local-public: "Veřejný (pouze místní)"
local-home: "Domovský (pouze místní)"
public: "Veřejné"
home: "Domů"
specified-desc: "Poslat pouze zmíněným uživatelům"
local-public: "Veřejná (pouze místní)"
local-home: "Domovská (pouze místní)"
local-followers: "Pro sledující (pouze místní)"
common/views/components/trends.vue:
count: "{} zmíněných uživatelů"
empty: "Žádný trend"
common/views/components/language-settings.vue:
title: "Zobrazit jazyky"
pick-language: "Zvolte jazyk"
recommended: "Doporučené"
info: "Pro aktivování změn musíte znovu načíst stránky."
common/views/components/profile-editor.vue:
title: "Profil"
name: "Jméno"
account: "Účet"
location: "Lokace"
description: "O mně"
you-can-include-hashtags: "V popisku o Vás můžete použít i hastagy."
language: "Jazyk"
birthday: "Datum narození"
avatar: "Avatar"
banner: "Baner"
is-cat: "Tento účet je kočka"
is-bot: "Tento účet je Bot"
advanced: "Ostatní"
privacy: "Osobní údaje"
save: "Uložit"
@@ -386,9 +556,10 @@ common/views/components/profile-editor.vue:
email-not-verified: "Váš email není potvrzen. Prosím zkontrolujte si svou schránku."
export: "Exportovat"
export-targets:
following-list: "Seznam sledovníků"
following-list: "Seznam sledujících"
mute-list: "Seznam ztlumených uživatelů"
blocking-list: "Seznam blokovaných uživatelů"
user-lists: "Seznamy"
enter-password: "Prosím, zadejte Vaše heslo"
danger-zone: "Nebezpečná zóna"
delete-account: "Smazat účet"
@@ -401,6 +572,7 @@ common/views/components/user-list-editor.vue:
delete-are-you-sure: "Smazat seznam \"$1\"?"
deleted: "Smazáno"
common/views/widgets/broadcast.vue:
fetching: "Načítám"
next: "Další"
common/views/widgets/calendar.vue:
year: "Rok {}"
@@ -413,10 +585,12 @@ common/views/widgets/photo-stream.vue:
no-photos: "Žádné obrázky"
common/views/widgets/posts-monitor.vue:
title: "Grafy příspěvků"
toggle: "Přepnout zobrazení"
common/views/widgets/hashtags.vue:
title: "Hashtagy"
common/views/widgets/server.vue:
title: "Informace o serveru"
toggle: "Přepnout zobrazení"
common/views/widgets/memo.vue:
title: "Poznámky"
memo: "Pište sem!"
@@ -425,12 +599,19 @@ common/views/widgets/slideshow.vue:
no-image: "V této složce nebyly nalezeny žádné fotky."
desktop:
banner: "Baner"
avatar-crop-title: "Vyberte část, která se zobrazí jako avatar"
avatar: "Avatar"
uploading-avatar: "Nahrál nový avatar"
avatar-updated: "Vaše avatar byl aktualizován"
invalid-filetype: "Tento formát souboru není podporován"
desktop/views/components/activity.chart.vue:
total: "Černá ... Celkem"
notes: "Modrá ... Poznámky"
replies: "Červená ... Odpovědi"
renotes: "Zelená ... Renoty"
desktop/views/components/activity.vue:
title: "Aktivita"
toggle: "Přepnout zobrazení"
desktop/views/components/calendar.vue:
title: "{month}. {year}"
prev: "Předchozí měsíc"
@@ -448,6 +629,8 @@ desktop/views/components/choose-folder-from-drive-window.vue:
desktop/views/components/crop-window.vue:
cancel: "Zrušit"
ok: "OK"
desktop/views/components/drive-window.vue:
used: "využito"
desktop/views/components/drive.file.vue:
avatar: "Avatar"
banner: "Baner"
@@ -479,10 +662,41 @@ desktop/views/components/drive.vue:
empty-folder: "Tato složka je prázdná"
unable-to-process: "Operace nemohla být dokončena."
unhandled-error: "Neznámá chyba"
url-upload: "Nahrát z URL adresy"
url-of-file: "URL adresa souboru, který chcete nahrát"
may-take-time: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
create-folder: "Vytvořit složku"
folder-name: "Název složky"
contextmenu:
create-folder: "Vytvořit složku"
upload: "Nahrát soubor"
url-upload: "Nahrát z URL"
desktop/views/components/media-video.vue:
click-to-show: "Klikněte pro zobrazení"
desktop/views/components/game-window.vue:
game: "Reversi"
desktop/views/components/home.vue:
done: "Hotovo"
add: "Přidat"
desktop/views/input-dialog.vue:
cancel: "Zrušit"
ok: "OK"
desktop/views/components/messaging-room-window.vue:
title: "Zprávy:"
desktop/views/components/messaging-window.vue:
title: "Zprávy"
desktop/views/components/note-detail.vue:
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
renote: "Renotovat"
add-reaction: "Přidat reakci"
undo-reaction: "Odebrat reakci"
desktop/views/components/note.vue:
reply: "Odpovědět"
renote: "Renote"
add-reaction: "Přidat reakci"
undo-reaction: "Odebrat reakci"
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
desktop/views/components/notes.vue:
error: "Načítání selhalo."
@@ -493,37 +707,62 @@ desktop/views/components/post-form.vue:
hide-contents: "Schovat obsah"
reply-placeholder: "Odpovědět na tento příspěvěk"
quote-placeholder: "Citovat tento příspěvek"
submit: "Příspěvek"
reply: "Odpovědět"
renote: "Renotovat"
posted: "Odesláno!"
replied: "Odpověděno!"
reposted: "Renotováno!"
note-failed: "Nepodařilo se přidat příspěvek"
renote-failed: "Renotování neuspělo"
insert-a-kao: "v('ω')v"
create-poll: "Vytvořit anketu"
text-remain: "{0} znaků zbývá"
recent-tags: "Nejnovější"
visibility: "Viditelnost"
geolocation-alert: "Vaše zařízení nepodporuje lokační službu"
error: "Chyba"
enter-username: "Zadejte své uživatelské jméno..."
desktop/views/components/post-form-window.vue:
note: "Nový příspěvek"
reply: "Odpovědět"
desktop/views/components/progress-dialog.vue:
waiting: "Čekáme"
desktop/views/components/renote-form.vue:
quote: "Citovat..."
cancel: "Zrušit"
renote: "Renotovat"
renote-home: "Renote (domů)"
reposting: "Renotuji..."
success: "Renotováno!"
failure: "Renotování neuspělo"
desktop/views/components/renote-form-window.vue:
title: "Chcete tohle renotovat?"
desktop/views/components/settings.2fa.vue:
detail: "Více…"
url: "https://www.google.cz/landing/2step/"
common/views/components/media-image.vue:
click-to-show: "Klikněte pro zobrazení"
common/views/components/api-settings.vue:
token: "Token:"
enter-password: "Prosím zadejte heslo"
console:
title: "API konzole"
endpoint: "Endpoint"
parameter: "Parametry"
send: "Odeslat"
sending: "Odesílám"
response: "Výsledek"
desktop/views/components/settings.apps.vue:
no-apps: "Žádné připojené aplikace"
common/views/components/drive-settings.vue:
max: "Velikost úložiště"
in-use: "využito"
stats: "Statistiky"
common/views/components/mute-and-block.vue:
mute-and-block: "Umlčet / Blokovat"
mute-and-block: "Umlčet/blokovat"
mute: "Umlčet"
block: "Blokováno"
block: "Blokován"
no-muted-users: "Žádný uživatel nebyl umlčen"
no-blocked-users: "Žádný uživatel není blokován"
save: "Uložit"
@@ -546,21 +785,55 @@ desktop/views/components/settings.tags.vue:
desktop/views/components/taskmanager.vue:
title: "Správce úloh"
desktop/views/components/timeline.vue:
home: "Domů"
local: "Lokální"
global: "Globální"
mentions: "Zmínění"
messages: "Zprávy"
list: "Seznamy"
hashtag: "Hashtag"
add-list: "Přidat do seznamu"
list-name: "Název seznamu"
desktop/views/components/ui.header.vue:
welcome-back: "Vítejte zpátky,"
adjective: "Pán"
desktop/views/components/ui.header.account.vue:
profile: "Váš profil"
lists: "Seznamy"
admin: "Administrace"
desktop/views/components/ui.header.nav.vue:
game: "Hry"
desktop/views/components/ui.header.notifications.vue:
title: "Oznámení"
desktop/views/components/ui.header.post.vue:
post: "Nový příspěvek"
desktop/views/components/ui.header.search.vue:
placeholder: "Vyhledávání"
desktop/views/components/received-follow-requests-window.vue:
accept: "Přijmout"
reject: "Odmítnout"
desktop/views/components/user-lists-window.vue:
title: "Seznamy uživatelů"
create-list: "Vytvořit seznam"
list-name: "Název seznamu"
desktop/views/components/user-preview.vue:
notes: "Příspěvky"
desktop/views/components/users-list.vue:
all: "Všechny"
iknow: "Znáte"
fetching: "Načítám…"
desktop/views/components/window.vue:
close: "Zavřít"
admin/views/index.vue:
instance: "Instance"
emoji: "Emoji"
moderators: "Moderátoři"
users: "Uživatelé"
federation: "Z fediversu"
announcements: "Oznámení"
hashtags: "Hashtagy"
queue: "Fronta úloh"
logs: "Logy"
back-to-misskey: "Zpět na Misskey"
admin/views/dashboard.vue:
accounts: "Účty"
@@ -611,9 +884,19 @@ admin/views/instance.vue:
saved: "Uloženo"
user-recommendation-config: "Doporučení uživatelé"
email: "Emailová adresa"
smtp-port: "SMTP Port"
smtp-auth: "Provést SMTP autentikaci"
smtp-user: "SMTP uživatel"
smtp-pass: "SMTP heslo"
serviceworker-config: "ServiceWorker"
enable-serviceworker: "Povolit ServiceWorker"
vapid-publickey: "VAPID veřejný klíč"
vapid-privatekey: "VAPID osobní klíč"
admin/views/charts.vue:
title: "Graf"
per-day: "za den"
per-hour: "za hodinu"
federation: "Federace"
notes: "Příspěvky"
users: "Uživatelé"
drive: "Disk"
@@ -621,11 +904,20 @@ admin/views/charts.vue:
charts:
federation-instances: "Počet instancí: zvýšení/snížení"
federation-instances-total: "Celkový počet instancí"
notes-total: "Celkem příspěvků"
users-total: "Celkem uživatelů"
active-users: "Aktivní uživatelé"
network-requests: "Požadavek"
network-time: "Doba odezvy"
network-usage: "Síťový provoz"
admin/views/drive.vue:
operation: "Operace"
fileid-or-url: "ID nebo URL souboru"
file-not-found: "Soubor nebyl nalezen"
sort:
title: "Seřadit"
createdAtAsc: "Věk - od nejstaršího"
createdAtDesc: "Věk - od nejmladšího"
sizeAsc: "Velikost - od nejmenších"
sizeDesc: "Velikost od největších"
origin:
@@ -642,8 +934,17 @@ admin/views/users.vue:
reset-password: "Resetovat heslo"
reset-password-confirm: "Opravdu chcete resetovat Vaše heslo?"
password-updated: "Heslo je nyní \"{password}\""
verify: "Ověřit účet"
verify-confirm: "Chcete aby toto byl ověřený účet?"
verified: "Účet se nyní ověřuje"
unverify: "Zrušit ověření účtu"
unverify-confirm: "Opravdu chcete zrušit designaci \"ověřený účet\"?"
unverified: "Ruší se potvrzení účtu"
update-remote-user: "Aktualizovat informace o vzdáleném účtu"
users:
title: "Uživatel"
state:
all: "Všechny"
moderator: "Moderátor"
adminOrModerator: "Admin/Moderátor"
verified: "Ověřený účet"
@@ -695,61 +996,182 @@ admin/views/federation.vue:
users: "Uživatelé"
status: "Status"
latest-request-received-at: "Poslední požadavek přijat"
block: "Blokováno"
block: "Blokován"
instances: "Instance"
states:
blocked: "Blokováno"
all: "Všechny"
blocked: "Blokován"
not-responding: "Bez odpovědi"
marked-as-closed: "Označeno jako uzavřené"
charts: "Graf"
chart-srcs:
requests: "Požadavek"
users-total: "Celkem uživatelů"
notes-total: "Celkem příspěvků"
chart-spans:
hour: "za hodinu"
day: "za den"
desktop/views/pages/welcome.vue:
about: "O Misskey"
timeline: "Časová osa"
announcements: "Oznámení"
photos: "Nedávné obrázky"
powered-by-misskey: "Běží na <b>Misskey</b>."
info: "Informace"
desktop/views/pages/drive.vue:
title: "Misskey Disk"
desktop/views/pages/note.vue:
prev: "Předchozí příspěvěk"
next: "Následující příspěvek"
desktop/views/pages/selectdrive.vue:
title: "Vyberte soubor(y)"
ok: "OK"
cancel: "Zrušit"
upload: "Nahrajte soubory z vašeho zařízení"
desktop/views/pages/search.vue:
not-available: "Vyhledávání je vypnuté pro tuto instanci."
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
desktop/views/pages/share.vue:
share-with: "Sdílet na {name}"
desktop/views/pages/tag.vue:
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
desktop/views/pages/user-list.users.vue:
users: "Uživatel"
add-user: "Přidat uživatele"
username: "Přezdívka"
desktop/views/pages/user/user.followers-you-know.vue:
loading: "Nahrávám..."
loading: "Načítám..."
desktop/views/pages/user/user.friends.vue:
loading: "Nahrávám..."
title: "Častá zmínění"
loading: "Načítám..."
no-users: "Žádná častá zmínění"
desktop/views/pages/user/user.photos.vue:
loading: "Nahrávám..."
title: "Fotky"
loading: "Načítám..."
no-photos: "Žádné obrázky"
desktop/views/pages/user/user.header.vue:
posts: "Poznámky"
month: "Po"
day: "Ne"
desktop/views/widgets/messaging.vue:
title: "Zprávy"
desktop/views/widgets/notifications.vue:
title: "Oznámení"
desktop/views/widgets/polls.vue:
title: "Ankety"
desktop/views/widgets/users.vue:
title: "Doporučení uživatelé"
mobile/views/components/drive.vue:
used: "využito"
file-count: "Soubor(ů)"
folder-is-empty: "Tato složka je prázdná"
deletion-alert: "Omlouváme se, ale mazání složek ještě nebylo implementováno."
folder-name: "Název složky"
url-prompt: "URL adresa souboru, který chcete nahrát"
uploading: "Byl zahájen upload. Může chvilku trvat než bude dokončen."
mobile/views/components/drive-file-chooser.vue:
select-file: "Vybrat soubory"
mobile/views/components/drive-folder-chooser.vue:
select-folder: "Vyberte složku"
mobile/views/components/drive.file-detail.vue:
download: "Stáhnout"
rename: "Přejmenovat"
move: "Přesunout"
hash: "Hash (md5)"
exif: "EXIF"
mobile/views/components/media-video.vue:
click-to-show: "Klikněte pro zobrazení"
common/views/components/follow-button.vue:
follow-processing: "Zpracovávám"
mobile/views/components/note.vue:
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
location: "Lokace"
mobile/views/components/note-detail.vue:
reply: "Odpovědět"
reaction: "Reakce"
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
location: "Lokace"
mobile/views/components/note-preview.vue:
admin: "admin"
bot: "bot"
cat: "kočka"
mobile/views/components/note-sub.vue:
admin: "admin"
bot: "bot"
cat: "kočka"
mobile/views/components/post-form.vue:
add-visible-user: "Přidat uživatele"
submit: "Příspěvek"
reply: "Odpovědět"
renote: "Renotovat"
reply-placeholder: "Odpovědět na tento příspěvěk"
location-alert: "Vaše zařízení nepodporuje lokační službu"
error: "Chyba"
username-prompt: "Zadejte uživatelské jméno"
mobile/views/components/sub-note-content.vue:
private: "Tento příspěvek je soukromý"
deleted: "Tento příspěvek byl odstraněn"
poll: "Ankety"
mobile/views/components/ui.header.vue:
welcome-back: "Vítejte zpátky,"
adjective: "Pán"
mobile/views/components/ui.nav.vue:
timeline: "Časová osa"
notifications: "Oznámení"
search: "Vyhledávání"
user-lists: "Seznamy"
widgets: "Widgety"
game: "Hry"
admin: "Administrace"
about: "O Misskey"
mobile/views/pages/user-lists.vue:
title: "Seznamy"
mobile/views/pages/signup.vue:
lets-start: "Váš účet je připraven! 📦"
mobile/views/pages/home.vue:
home: "Domů"
local: "Lokální"
global: "Globální"
mentions: "Zmínění"
messages: "Zprávy"
mobile/views/pages/tag.vue:
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
mobile/views/pages/widgets.vue:
add-widget: "Přidat"
customization-tips: "Tipy pro přizpůsobení"
mobile/views/pages/widgets/activity.vue:
activity: "Aktivita"
mobile/views/pages/share.vue:
share-with: "Sdílet na {name}"
mobile/views/pages/received-follow-requests.vue:
accept: "Přijmout"
reject: "Odmítnout"
mobile/views/pages/note.vue:
prev: "Předchozí příspěvěk"
next: "Následující příspěvek"
mobile/views/pages/games/reversi.vue:
reversi: "Reversi"
mobile/views/pages/search.vue:
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
mobile/views/pages/selectdrive.vue:
select-file: "Vybrat soubory"
mobile/views/pages/user/home.vue:
activity: "Aktivita"
frequently-replied-users: "Častá zmínění"
mobile/views/pages/user/home.photos.vue:
no-photos: "Žádné obrázky"
deck:
widgets: "Widgety"
home: "Domů"
local: "Lokální"
hashtag: "Hashtagy"
global: "Globální"
mentions: "Zmínění"
notifications: "Oznámení"
list: "Seznamy"
select-list: "Vyberte seznam"
swap-left: "Posunout doleva"
swap-right: "Posunout doprava"
rename: "Přejmenovat"
@@ -758,4 +1180,10 @@ deck/deck.user-column.vue:
dev/views/new-app.vue:
app-name-desc: "Jméno vaší aplikace"
app-desc: "Stručný popis nebo představení vaší aplikace."
account-read: "Zobrazit informace účtu"
note-write: "Odeslat."
reaction-write: "Přidat nebo odebrat reakce."
following-write: "Sledovat a přestat sledovat"
drive-read: "Přečíst váš Disk"
notification-read: "Sledovat oznámení."
notification-write: "Zpravovat notifikace."

View File

@@ -270,7 +270,6 @@ common/views/components/note-menu.vue:
common/views/components/poll.vue:
vote-to: "Stimme für '{}'"
vote-count: "{} Stimmen"
total-users: "{} Nutzer haben abgestimmt"
vote: "Abstimmen"
show-result: "Zeige Ergebnis"
voted: "Abgestimmt"
@@ -280,6 +279,7 @@ common/views/components/poll-editor.vue:
remove: "Diese Auswahl entfernen"
add: "+ Eine Auswahl hinzufügen"
destroy: "Diese Abstimmung löschen"
day: "So"
common/views/components/reaction-picker.vue:
choose-reaction: "Wähle eine Reaktion aus"
common/views/components/emoji-picker.vue:
@@ -339,6 +339,8 @@ common/views/components/profile-editor.vue:
banner: "Banner"
save: "Speichern"
export: "Exportieren"
export-targets:
user-lists: "Listen"
enter-password: "Bitte Passwort eingeben"
common/views/widgets/broadcast.vue:
fetching: "Laden"

View File

@@ -24,8 +24,8 @@ common:
application-authorization: "Application authorizations"
close: "Close"
do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
load-more: "Load more"
enter-password: "Please enter the Password"
load-more: "Read more"
enter-password: "Enter your password"
2fa: "Two-factor authentication"
customize-home: "Customize home layout"
featured-notes: "Featured notes"
@@ -114,7 +114,7 @@ common:
a: "What are you doing?"
b: "What's happening?"
c: "Whats on your mind?"
d: "Would you post any words?"
d: "What do you want to say?"
e: "Write here"
f: "Waiting for your writing."
settings: "Settings"
@@ -223,8 +223,8 @@ common:
search: "Search"
delete: "Delete"
loading: "Loading"
ok: "It's OK"
cancel: "Quit"
ok: "Confirm"
cancel: "Exit"
update-available-title: "Update available"
update-available: "A new version of Misskey is now available({newer}, the current version is {current}). Reload the page to apply updates."
my-token-regenerated: "Your token has been regenerated, so you will be signed out."
@@ -285,7 +285,7 @@ auth/views/form.vue:
account-read: "View account information."
account-write: "Modify account information."
note-write: "Post."
like-write: "React to posts."
like-write: "Express yourself about this post."
following-write: "Follow and unfollow."
drive-read: "Read your drive."
drive-write: "Upload/delete files in your drive."
@@ -304,7 +304,7 @@ auth/views/index.vue:
error: "Session does not exist."
sign-in: "Please sign in."
common/views/pages/explore.vue:
verified-users: "Verified accounts"
verified-users: "Official accounts"
popular-users: "Popular users"
recently-updated-users: "Recently active users"
recently-registered-users: "Users who joined recently"
@@ -489,16 +489,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "Vote for '{}'"
vote-count: "{} votes"
total-users: "{} users voted"
total-votes: "{} votes in total"
vote: "Vote"
show-result: "Show results"
voted: "Voted"
closed: "Ended"
remaining-days: "{d} days, {h} hours remain"
remaining-hours: "{h} hours, and {m} minutes remain"
remaining-minutes: "{m} minutes, and {s} seconds remaining"
remaining-seconds: "{s} seconds remaining"
common/views/components/poll-editor.vue:
no-only-one-choice: "At least two choices are required"
choice-n: "Choice {}"
remove: "Delete the choice"
add: "+ Add a choice"
destroy: "Discard the poll"
multiple: "More than one answer is allowed"
expiration: "Valid until"
infinite: "Indefinitely"
at: "Date and time pick"
after: "Progression specifics"
no-more: "You cannot add any more"
deadline-date: "Finish date"
deadline-time: "Time duration"
interval: "Duration"
unit: "Unit"
second: "Seconds"
minute: "Minutes"
hour: "Hours"
day: "S"
common/views/components/reaction-picker.vue:
choose-reaction: "Send a reaction"
common/views/components/emoji-picker.vue:
@@ -520,7 +539,7 @@ common/views/components/signin.vue:
signin-with-twitter: "Log in with Twitter"
signin-with-github: "Sign in with GitHub"
signin-with-discord: "Sign in with Discord"
login-failed: "Log in failed. Make sure you have entered your correct username and password."
login-failed: "Logging in has failed. Make sure you have entered the correct username and password."
common/views/components/signup.vue:
invitation-code: "Invitation code"
invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>."
@@ -633,6 +652,7 @@ common/views/components/profile-editor.vue:
following-list: "List of followers"
mute-list: "List of muted accounts"
blocking-list: "List of blocked accounts"
user-lists: "Lists"
export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
enter-password: "Please enter your password"
danger-zone: "Cautious options"
@@ -1196,7 +1216,7 @@ admin/views/users.vue:
updatedAtAsc: "Last Updated (Ascending)"
updatedAtDesc: "Last Updated (Descending)"
state:
title: "Status"
title: "Sort"
all: "All"
admin: "Administrator"
moderator: "Moderator"
@@ -1257,7 +1277,7 @@ admin/views/federation.vue:
users: "Users"
following: "Following"
followers: "Followers"
status: "Status"
status: "Statuses"
latest-request-sent-at: "Time of last request sent"
latest-request-received-at: "Last request received at"
remove-all-following: "Withold all followers"
@@ -1273,19 +1293,19 @@ admin/views/federation.vue:
caughtAtDesc: "Date of discovery (Descending)"
lastCommunicatedAtAsc: "The date and time of the older interactions"
lastCommunicatedAtDesc: "The date and time of the newer interactions"
notesAsc: "Order by least Notes posted"
notesDesc: "Order by most Notes posted"
notesAsc: "Least Notes posted"
notesDesc: "Most Notes posted"
usersAsc: "Less followers"
usersDesc: "More followers"
followingAsc: "Least followed"
followingDesc: "Has more followers"
followersAsc: "Sort by having less followers"
followersDesc: "Sort by the larger number of followers"
followingDesc: "Most followed"
followersAsc: "Having less followers"
followersDesc: "The largest number of followers"
driveUsageAsc: "Least storage used"
driveUsageDesc: "Most storage used"
driveFilesAsc: "By the smallest number of files stored on Drive"
driveFilesDesc: "By the largest number of files stored on Drive"
state: "Status"
driveFilesAsc: "Least files stored on Drive"
driveFilesDesc: "The largest number of files stored on Drive"
state: "Sort"
states:
all: "All"
blocked: "Blocked"
@@ -1353,7 +1373,7 @@ desktop/views/pages/user/user.header.vue:
following: "Following"
followers: "Followers"
is-bot: "This account is a Bot"
no-description: "The user has not written their profile introduction"
no-description: "This user has not written their profile introduction"
years-old: "{age} years old"
year: "/"
month: "/"
@@ -1365,7 +1385,7 @@ desktop/views/pages/user/user.timeline.vue:
with-media: "Media"
my-posts: "My posts"
desktop/views/widgets/messaging.vue:
title: "Message"
title: "Messaging"
desktop/views/widgets/notifications.vue:
title: "Notifications"
desktop/views/widgets/polls.vue:

View File

@@ -103,6 +103,32 @@ common:
tags: "Etiquetas"
blocking: "Bloquear"
password: "Contraseña"
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
line-width: "Grosor de línea"
line-width-thick: "Grosor"
font-size: "Tamaño del texto"
font-size-x-small: "Muy pequeño"
font-size-small: "Pequeño"
font-size-medium: "Normal"
font-size-large: "Grande"
font-size-x-large: "Muy grande"
deck-column-align: "Alineamiento de las columnas"
deck-column-align-center: "Centrar"
deck-column-align-left: "Izquierda"
deck-column-align-flexible: "Flexible"
deck-column-width: "Ancho de las columnas"
deck-column-width-narrow: "Estrecho"
deck-column-width-narrower: "Un poco estrecho"
deck-column-width-normal: "Normal"
deck-column-width-wider: "Un poco ancho"
deck-column-width-wide: "Ancho"
use-shadow: "Usar sombras en la Interfaz de Usuario"
rounded-corners: "Esquinas redondeadas en la Interfaz de Usuario"
circle-icons: "Usar iconos circulares"
contrasted-acct: "Añadir contraste al nombre de usuario"
wallpaper: "Fondo de pantalla"
choose-wallpaper: "Escoge un fondo de pantalla"
navbar-position-left: "Izquierda"
search: "Buscar"
delete: "eliminar"
loading: "cargando"
@@ -303,7 +329,6 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "'{}' para votar"
vote-count: "{} votos"
total-users: "{} usuario(s) que ha(n) votado"
vote: "Vota"
show-result: "Mostrar resultados"
voted: "Votado"
@@ -313,6 +338,7 @@ common/views/components/poll-editor.vue:
remove: "Borra la opción"
add: "+ Añade una opción"
destroy: "Cancelar la encuesta"
day: "domingo"
common/views/components/reaction-picker.vue:
choose-reaction: "Escoge una reacción"
common/views/components/emoji-picker.vue:
@@ -398,6 +424,7 @@ common/views/components/profile-editor.vue:
export-targets:
mute-list: "Silenciar"
blocking-list: "Bloquear"
user-lists: "Listas"
enter-password: "Escribe una contraseña"
common/views/components/user-list-editor.vue:
users: "Usuarios"

View File

@@ -383,7 +383,6 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "Voter pour '{}'"
vote-count: "{} votes"
total-users: "{} utilisateur·rice·s ont voté"
vote: "Vote"
show-result: "Montrer les résultats"
voted: "Voté"
@@ -393,6 +392,7 @@ common/views/components/poll-editor.vue:
remove: "Supprimer ce choix"
add: "+ Ajouter un choix"
destroy: "Annuler ce sondage"
day: "D"
common/views/components/reaction-picker.vue:
choose-reaction: "Choisissez votre réaction"
common/views/components/emoji-picker.vue:
@@ -527,6 +527,7 @@ common/views/components/profile-editor.vue:
following-list: "Liste des abonnements"
mute-list: "Liste des comptes mis en sourdine"
blocking-list: "Liste des comptes bloqués"
user-lists: "Listes"
export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
enter-password: "Veuillez saisir votre mot de passe"
danger-zone: "Zone de danger"

View File

@@ -5,22 +5,46 @@
const fs = require('fs');
const yaml = require('js-yaml');
const langs = ['de-DE', 'en-US', 'fr-FR', 'ja-JP', 'ja-KS', 'pl-PL', 'es-ES', 'nl-NL', 'zh-CN', 'ko-KR'];
const merge = (...args) => args.reduce((a, c) => ({
...a,
...c,
...Object.entries(a)
.filter(([k]) => c && typeof c[k] === 'object')
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
}), {});
const loadLocale = lang => yaml.safeLoad(fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
const locales = langs
.map(lang => [lang, loadLocale(lang)])
.map(([lang, locale], _, locales) => {
switch (lang) {
case 'ja-JP': return [lang, locale];
case 'en-US': return [lang, { ...locales['ja-JP'], ...locale }];
default: return [lang, {
...(lang.startsWith('ja-') ? {} : locales['en-US']),
...locales['ja-JP'],
...locale
}];
const languages = [
'de-DE',
'en-US',
'es-ES',
'fr-FR',
'ja-JP',
'ja-KS',
'ko-KR',
'nl-NL',
'pl-PL',
'zh-CN',
];
const primaries = {
'ja': 'JP',
'zh': 'CN',
};
const locales = languages.reduce((a, c) => (a[c] = yaml.safeLoad(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8')) || {}, a), {});
module.exports = Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => {
const [lang] = k.split('-');
switch (k) {
case 'ja-JP': return v;
case 'ja-KS':
case 'en-US': return merge(locales['ja-JP'], v);
default: return merge(
locales['ja-JP'],
locales['en-US'],
locales[`${lang}-${primaries[lang]}`] || {},
v
);
}
})
.map(([lang, locale]) => ({ [lang]: loadLocale(lang) }));
module.exports = locales.reduce((a, b) => ({ ...a, ...b }));
})(), a), {});

View File

@@ -1,6 +1,6 @@
---
meta:
lang: "In Italiano"
lang: "Italiano"
common:
misskey: "A ⭐ of the fediverse"
about-title: "A ⭐ of the fediverse."

View File

@@ -527,10 +527,15 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "「{}」に投票する"
vote-count: "{}票"
total-users: "{}人が投票"
total-votes: "{}票"
vote: "投票する"
show-result: "結果を見る"
voted: "投票済み"
closed: "終了済み"
remaining-days: "終了まであと{d}日{h}時間"
remaining-hours: "終了まであと{h}時間{m}分"
remaining-minutes: "終了まであと{m}分{s}秒"
remaining-seconds: "終了まであと{s}秒"
common/views/components/poll-editor.vue:
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
@@ -538,6 +543,20 @@ common/views/components/poll-editor.vue:
remove: "この選択肢を削除"
add: "+選択肢を追加"
destroy: "アンケートを破棄"
multiple: "複数回答可"
expiration: "期限"
infinite: "無期限"
at: "日時指定"
after: "経過指定"
no-more: "これ以上追加できません"
deadline-date: "期日"
deadline-time: "時間"
interval: "期間"
unit: "単位"
second: "秒"
minute: "分"
hour: "時間"
day: "日"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
@@ -687,6 +706,7 @@ common/views/components/profile-editor.vue:
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
user-lists: "リスト"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
enter-password: "パスワードを入力してください"
danger-zone: "危険な設定"

View File

@@ -344,7 +344,6 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "「{}」に投票や!"
vote-count: "{}票"
total-users: "{}人が投票"
vote: "投票するで"
show-result: "結果を見よか"
voted: "投票済みや"
@@ -354,6 +353,7 @@ common/views/components/poll-editor.vue:
remove: "この選択肢を消すで"
add: "+選択肢を追加"
destroy: "アンケートをほかそ"
day: "日"
common/views/components/reaction-picker.vue:
choose-reaction: "リアクション、どれにするんや?"
common/views/components/emoji-picker.vue:
@@ -480,6 +480,7 @@ common/views/components/profile-editor.vue:
following-list: "フォロー"
mute-list: "ミュート"
blocking-list: "ブロック"
user-lists: "リスト"
enter-password: "パスワードを入れてや"
common/views/components/user-list-editor.vue:
users: "ユーザー"

View File

@@ -489,16 +489,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "\"{}\"에 투표하기"
vote-count: "{}표"
total-users: "{}명이 투표"
total-votes: "{}표"
vote: "투표하기"
show-result: "결과 보기"
voted: "투표함"
closed: "종료됨"
remaining-days: "종료까지 앞으로 {d}일 {h}시간"
remaining-hours: "종료까지 앞으로 {h}시간 {m}분"
remaining-minutes: "종료까지 앞으로 {m}분 {s}초"
remaining-seconds: "종료까지 앞으로 {s}초"
common/views/components/poll-editor.vue:
no-only-one-choice: "투표에는 선택지가 최소한 두 개 필요합니다"
choice-n: "선택지 {}"
remove: "이 선택지를 제거"
add: "+선택지 추가"
destroy: "투표 제거"
multiple: "복수 응답 가능"
expiration: "기한"
infinite: "무기한"
at: "일시 지정"
after: "기간 지정"
no-more: "더 이상 추가할 수 없습니다"
deadline-date: "기한"
deadline-time: "시간"
interval: "기간"
unit: "단위"
second: "초"
minute: "분"
hour: "시간"
day: "일"
common/views/components/reaction-picker.vue:
choose-reaction: "반응 선택"
common/views/components/emoji-picker.vue:
@@ -633,6 +652,7 @@ common/views/components/profile-editor.vue:
following-list: "팔로잉"
mute-list: "뮤트"
blocking-list: "차단"
user-lists: "리스트"
export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
enter-password: "비밀번호를 입력하여 주십시오"
danger-zone: "위험한 설정"

View File

@@ -131,7 +131,6 @@ common/views/components/note-menu.vue:
common/views/components/poll.vue:
vote-to: "Stemmen op '{}'"
vote-count: "{} stemmen"
total-users: "{} gebruikers hebben gestemd"
vote: "Stemmen"
show-result: "Resultaten tonen"
voted: "Gestemd"
@@ -141,6 +140,7 @@ common/views/components/poll-editor.vue:
remove: "Deze keuze verwijderen"
add: "+ Keuze toevoegen"
destroy: "Deze peiling vernietigen"
day: "Z"
common/views/components/reaction-picker.vue:
choose-reaction: "Kies een reactie"
common/views/components/emoji-picker.vue:
@@ -196,6 +196,7 @@ common/views/components/profile-editor.vue:
banner: "Omslagfoto"
export-targets:
following-list: "Volgend"
user-lists: "Lijsten"
enter-password: "Voer het wachtwoord in"
common/views/components/user-list-editor.vue:
users: "Gebruiker"

View File

@@ -151,6 +151,7 @@ common/views/components/poll.vue:
voted: "Stemt"
common/views/components/poll-editor.vue:
choice-n: "Valg {}"
day: "S"
common/views/components/signin.vue:
username: "Brukernavn"
password: "Passord"
@@ -186,6 +187,7 @@ common/views/components/profile-editor.vue:
save: "Lagre"
export-targets:
following-list: "Følger"
user-lists: "Lister"
common/views/components/user-list-editor.vue:
users: "Bruker"
common/views/widgets/broadcast.vue:

View File

@@ -26,6 +26,7 @@ common:
dark-mode: "Tryb ciemny"
signin: "Zaloguj się"
signup: "Rejestracja"
signout: "Wyloguj się"
got-it: "Rozumiem!"
customization-tips:
title: "Wskazówki o dostosowywaniu"
@@ -120,7 +121,24 @@ common:
other: "Inne"
appearance: "Wygląd"
behavior: "Zachowanie"
note-visibility: "Widoczność wpisów"
line-width-thin: "Cienka"
line-width-normal: "Normalna"
line-width-thick: "Gruba"
font-size: "Rozmiar tekstu"
font-size-medium: "Normalna"
font-size-x-large: "Duży"
deck-column-align-center: "Po środku"
deck-column-align-left: "Z lewej"
deck-column-align-flexible: "Elastyczne"
deck-column-width: "Szerokość kolumn w talii"
deck-column-width-narrow: "Wąska"
deck-column-width-narrower: "Trochę wąska"
deck-column-width-normal: "Normalna"
deck-column-width-wider: "Trochę szerokie"
deck-column-width-wide: "Szeroka"
timeline: "Oś czasu"
navbar-position-left: "Z lewej"
search: "Szukaj"
delete: "Usuń"
loading: "Ładowanie"
@@ -346,7 +364,6 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "Zagłosuj na '{}'"
vote-count: "{} głosów"
total-users: "{} głosujących"
vote: "Zagłosuj"
show-result: "Pokaż wyniki"
voted: "Zagłosowano"
@@ -356,6 +373,7 @@ common/views/components/poll-editor.vue:
remove: "Usuń tą opcję"
add: "+ Dodaj opcję"
destroy: "Usuń tę ankietę"
day: "N"
common/views/components/reaction-picker.vue:
choose-reaction: "Wybierz reakcję"
common/views/components/emoji-picker.vue:
@@ -476,6 +494,7 @@ common/views/components/profile-editor.vue:
following-list: "Śledzeni"
mute-list: "Wycisz"
blocking-list: "Zablokuj"
user-lists: "Listy"
enter-password: "Wprowadź hasło"
common/views/components/user-list-editor.vue:
users: "Użytkownicy"

View File

@@ -158,6 +158,8 @@ common/views/components/messaging.vue:
you: "Você"
common/views/components/note-menu.vue:
delete: "Apagar"
common/views/components/poll-editor.vue:
day: "Dom"
common/views/components/visibility-chooser.vue:
followers: "Seguidores"
common/views/components/profile-editor.vue:

View File

@@ -127,6 +127,8 @@ common/views/components/connect-failed.vue:
title: "Невозможно подключиться к серверу"
common/views/components/cw-button.vue:
poll: "Голосования"
common/views/components/poll-editor.vue:
day: "Вс"
common/views/widgets/memo.vue:
title: "Заметка"
desktop/views/components/sub-note-content.vue:

View File

@@ -489,16 +489,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue:
vote-to: "为\"{}\"投票"
vote-count: "{}票"
total-users: "{} 人投票"
total-votes: "总票数{}"
vote: "投票"
show-result: "显示结果"
voted: "已投票"
closed: "已截止"
remaining-days: "{d}天{h}小时后截止"
remaining-hours: "{h}小时{m}分后截止"
remaining-minutes: "{m}分{s}秒后截止"
remaining-seconds: "{s}秒后截止"
common/views/components/poll-editor.vue:
no-only-one-choice: "至少选择两个选项"
choice-n: "选择{}"
remove: "删除选项"
add: "+添加一个选项"
destroy: "放弃投票"
multiple: "允许多个投票"
expiration: "截止时间"
infinite: "永久"
at: "指定日期"
after: "指定时间"
no-more: "最多只能添加十个回答"
deadline-date: "日期"
deadline-time: "时间"
interval: "时长"
unit: "单位"
second: "秒"
minute: "分"
hour: "小时"
day: "日"
common/views/components/reaction-picker.vue:
choose-reaction: "选择回应"
common/views/components/emoji-picker.vue:
@@ -633,6 +652,7 @@ common/views/components/profile-editor.vue:
following-list: "关注列表"
mute-list: "屏蔽列表"
blocking-list: "黑名单"
user-lists: "列表"
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
enter-password: "请输入您的密码"
danger-zone: "危险选项"

88
locales/zh-TW.yml Normal file
View File

@@ -0,0 +1,88 @@
---
meta:
lang: "中文(繁体)"
common:
intro:
title: "什麽是 Misskey 呢?"
rich-contents: "發佈"
reaction: "回應"
drive: "雲端硬碟"
adblock:
detected: "請禁用廣告封鎖器"
close: "關閉"
enter-password: "請輸入密碼"
2fa: "雙重身份驗證"
dark-mode: "夜間模式"
signup: "註冊"
signout: "登出"
notification:
reversi-invited: "您已被邀請加入壹場遊戲"
reversi-invited-by: "來自{}的邀請"
notified-by: "來自{}的邀請"
time:
future: "未來"
just_now: "剛剛"
drive: "雲端硬碟"
weekday:
sunday: "週日"
monday: "週一"
tuesday: "週二"
wednesday: "週三"
thursday: "週四"
friday: "週五"
saturday: "週六"
reactions:
like: "贊"
love: "喜歡"
congrats: "恭喜"
_settings:
password: "密碼"
font-size: "字體大小"
font-size-x-small: "小"
font-size-small: "較小"
deck-column-width-wide: "寬"
timeline: "時間軸"
common/views/components/connect-failed.troubleshooter.vue:
flush: "清除快取"
common/views/components/theme.vue:
light-themes: "淺色主題"
dark-themes: "深色主題"
install-a-theme: "安裝主題"
save-created-theme: "保存主題"
common/views/components/signin.vue:
signin-with-twitter: "用 Twitter 帳號登入"
signin-with-github: "用 GitHub 帳號登入"
signin-with-discord: "用 Discord 帳號登入"
login-failed: "登錄失敗。 請檢查用戶名和密碼。"
common/views/components/signup.vue:
invitation-code: "邀請碼"
username: "用戶名"
available: "可用"
too-long: "請不要超過20個字元"
password: "密碼"
password-placeholder: "建議至少8個字元"
common/views/components/stream-indicator.vue:
connecting: "正在連線"
reconnecting: "正在重新連線"
connected: "已建立連線"
common/views/components/integration-settings.vue:
disconnect: "中斷連線"
common/views/components/github-setting.vue:
reconnect: "重新連線"
disconnect: "中斷連線"
common/views/components/discord-setting.vue:
reconnect: "重新連線"
disconnect: "中斷連線"
common/views/components/language-settings.vue:
recommended: "推薦"
auto: "自動"
specify-language: "指定語言"
common/views/components/profile-editor.vue:
title: "個人資料"
name: "名稱"
birthday: "生日:"
privacy: "隱私"
admin/views/dashboard.vue:
drive: "雲端硬碟"
admin/views/charts.vue:
drive: "雲端硬碟"

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.91.2",
"version": "10.92.4",
"codename": "nighthike",
"repository": {
"type": "git",
@@ -32,6 +32,7 @@
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.5.8",
"@types/chai-http": "3.0.5",
"@types/dateformat": "3.0.0",
"@types/deep-equal": "1.0.1",
@@ -58,7 +59,7 @@
"@types/koa-logger": "3.1.1",
"@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.39",
"@types/koa-router": "7.0.40",
"@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3",
@@ -66,7 +67,7 @@
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.5",
"@types/mongodb": "3.1.20",
"@types/node": "10.12.24",
"@types/node": "11.10.4",
"@types/nodemailer": "4.6.6",
"@types/nprogress": "0.0.29",
"@types/oauth": "0.9.1",
@@ -84,7 +85,7 @@
"@types/seedrandom": "2.4.27",
"@types/sharp": "0.21.2",
"@types/showdown": "1.9.2",
"@types/speakeasy": "2.0.3",
"@types/speakeasy": "2.0.4",
"@types/systeminformation": "3.23.1",
"@types/tinycolor2": "1.4.1",
"@types/tmp": "0.0.33",
@@ -100,8 +101,8 @@
"autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"bee-queue": "1.2.2",
"bootstrap-vue": "2.0.0-rc.11",
"bootstrap-vue": "2.0.0-rc.13",
"bull": "3.7.0",
"cafy": "15.1.0",
"chai": "4.2.0",
"chai-http": "4.2.1",
@@ -118,11 +119,11 @@
"elasticsearch": "15.3.1",
"emojilib": "2.4.0",
"escape-regexp": "0.0.1",
"eslint": "5.12.0",
"eslint": "5.15.0",
"eslint-plugin-vue": "5.2.2",
"eventemitter3": "3.1.0",
"feed": "2.0.2",
"file-type": "10.7.1",
"file-type": "10.9.0",
"fuckadblock": "3.2.1",
"gulp": "4.0.0",
"gulp-cssnano": "2.1.3",
@@ -136,7 +137,6 @@
"gulp-typescript": "5.0.0",
"gulp-uglify": "3.0.1",
"gulp-util": "3.0.8",
"gulp-yaml": "2.0.3",
"hard-source-webpack-plugin": "0.13.1",
"html-minifier": "3.5.21",
"http-signature": "1.2.0",
@@ -176,7 +176,6 @@
"nodemailer": "5.1.1",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
"os-utils": "0.0.14",
"parse5": "5.1.0",
"parsimmon": "1.12.0",
@@ -251,7 +250,7 @@
"web-push": "3.3.3",
"webfinger.js": "2.7.0",
"webpack": "4.28.4",
"webpack-cli": "3.2.1",
"webpack-cli": "3.2.3",
"websocket": "1.0.28",
"ws": "6.1.4",
"xev": "2.0.1"

View File

@@ -5,8 +5,7 @@ program
.version(pkg.version)
.option('--no-daemons', 'Disable daemon processes (for debbuging)')
.option('--disable-clustering', 'Disable clustering')
.option('--disable-queue', 'Disable job queue processing')
.option('--only-server', 'Run server only (without job queue)')
.option('--only-server', 'Run server only (without job queue processing)')
.option('--only-queue', 'Pocessing job queue only (without server)')
.option('--quiet', 'Suppress all logs')
.option('--verbose', 'Enable all logs')
@@ -15,7 +14,6 @@ program
.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.')
.parse(process.argv);
/*if (process.env.MK_DISABLE_QUEUE)*/ program.disableQueue = true;
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
export { program };

View File

@@ -85,11 +85,10 @@ export default Vue.extend({
<style lang="stylus" scoped>
.nqjzuvev
white-space nowrap
overflow auto
padding 8px
background #000
color #fff
font-size 14px
> code
display block

View File

@@ -2,6 +2,34 @@
<div>
<ui-card>
<template #title>{{ $t('operation') }}</template>
<section>
<header>Deliver</header>
<ui-horizon-group inputs v-if="stats">
<ui-input :value="stats.deliver.waiting | number" type="text" readonly>
<span>Waiting</span>
</ui-input>
<ui-input :value="stats.deliver.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="stats.deliver.active | number" type="text" readonly>
<span>Active</span>
</ui-input>
</ui-horizon-group>
</section>
<section>
<header>Inbox</header>
<ui-horizon-group inputs v-if="stats">
<ui-input :value="stats.inbox.waiting | number" type="text" readonly>
<span>Waiting</span>
</ui-input>
<ui-input :value="stats.inbox.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="stats.inbox.active | number" type="text" readonly>
<span>Active</span>
</ui-input>
</ui-horizon-group>
</section>
<section>
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
</section>
@@ -18,9 +46,26 @@ export default Vue.extend({
data() {
return {
stats: null
};
},
created() {
const fetchStats = () => {
this.$root.api('admin/queue/stats', {}, true).then(stats => {
this.stats = stats;
});
};
fetchStats();
const clock = setInterval(fetchStats, 1000);
this.$once('hook:beforeDestroy', () => {
clearInterval(clock);
});
},
methods: {
async removeAllJobs() {
const process = async () => {

View File

@@ -4,7 +4,7 @@
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
<img class="avatar" :src="user.avatarUrl" alt=""/>
<span class="name">
<mk-user-name :user="user"/>
<mk-user-name :user="user" :key="user.id"/>
</span>
<span class="username">@{{ user | acct }}</span>
</li>

View File

@@ -3,32 +3,31 @@
<header>
<button v-for="category in categories"
:title="category.text"
@click="go(category.ref)"
@click="go(category)"
:class="{ active: category.isActive }"
>
<fa :icon="category.icon" fixed-width/>
</button>
</header>
<div class="emojis" ref="emojis" @scroll.passive="onScroll">
<section v-for="category in categories" :ref="category.ref">
<header><fa :icon="category.icon" fixed-width/> {{ category.text }}</header>
<div v-if="category.name">
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === category.name)"
:title="emoji[0]"
@click="chosen(emoji[1].char)"
>
<mk-emoji :emoji="emoji[1].char"/>
</button>
</div>
<div v-else>
<button v-for="emoji in customEmojis"
:title="emoji.name"
@click="chosen(`:${emoji.name}:`)"
>
<img :src="emoji.url" :alt="emoji.name"/>
</button>
</div>
</section>
<div class="emojis">
<header><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
<div v-if="categories.find(x => x.isActive).name">
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === categories.find(x => x.isActive).name)"
:title="emoji[0]"
@click="chosen(emoji[1].char)"
:key="emoji[0]"
>
<mk-emoji :emoji="emoji[1].char"/>
</button>
</div>
<div v-else>
<button v-for="emoji in customEmojis"
:title="emoji.name"
@click="chosen(`:${emoji.name}:`)"
>
<img :src="emoji.url" :alt="emoji.name"/>
</button>
</div>
</div>
</div>
</template>
@@ -48,55 +47,46 @@ export default Vue.extend({
lib,
customEmojis: [],
categories: [{
ref: 'customEmojiSection',
text: this.$t('custom-emoji'),
icon: faAsterisk,
isActive: true
}, {
name: 'people',
ref: 'peopleSection',
text: this.$t('people'),
icon: ['far', 'laugh'],
isActive: false
}, {
name: 'animals_and_nature',
ref: 'animalsAndNatureSection',
text: this.$t('animals-and-nature'),
icon: faLeaf,
isActive: false
}, {
name: 'food_and_drink',
ref: 'foodAndDrinkSection',
text: this.$t('food-and-drink'),
icon: faUtensils,
isActive: false
}, {
name: 'activity',
ref: 'activitySection',
text: this.$t('activity'),
icon: faFutbol,
isActive: false
}, {
name: 'travel_and_places',
ref: 'travelAndPlacesSection',
text: this.$t('travel-and-places'),
icon: faCity,
isActive: false
}, {
name: 'objects',
ref: 'objectsSection',
text: this.$t('objects'),
icon: faDice,
isActive: false
}, {
name: 'symbols',
ref: 'symbolsSection',
text: this.$t('symbols'),
icon: faHeart,
isActive: false
}, {
name: 'flags',
ref: 'flagsSection',
text: this.$t('flags'),
icon: faFlag,
isActive: false
@@ -109,15 +99,9 @@ export default Vue.extend({
},
methods: {
go(ref) {
this.$refs.emojis.scrollTop = this.$refs[ref][0].offsetTop;
},
onScroll(e) {
for (const x of this.categories) {
const top = e.target.scrollTop;
const el = this.$refs[x.ref][0];
x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top;
go(category) {
for (const c of this.categories) {
c.isActive = c.name === category.name;
}
},
@@ -156,47 +140,46 @@ export default Vue.extend({
overflow-y auto
overflow-x hidden
> section
> header
position sticky
top 0
left 0
z-index 1
padding 8px
background var(--faceHeader)
color var(--text)
font-size 12px
> header
position sticky
top 0
left 0
z-index 1
padding 8px
background var(--faceHeader)
color var(--text)
font-size 12px
> div
display grid
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
gap 4px
padding 8px
> div
display grid
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
gap 4px
padding 8px
> button
padding 0
width 100%
> button
padding 0
width 100%
&:before
content ''
display block
width 1px
height 0
padding-bottom 100%
&:hover
> *
transform scale(1.2)
transition transform 0s
&:before
content ''
display block
width 1px
height 0
padding-bottom 100%
&:hover
> *
position absolute
top 0
left 0
width 100%
height 100%
font-size 28px
transition transform 0.2s ease
pointer-events none
transform scale(1.2)
transition transform 0s
> *
position absolute
top 0
left 0
width 100%
height 100%
font-size 28px
transition transform 0.2s ease
pointer-events none
</style>

View File

@@ -1,5 +1,5 @@
<template>
<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="View source on GitHub">
<a class="a" :href="repo" target="_blank" title="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
@@ -8,9 +8,25 @@
</a>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
repositoryUrl: 'https://github.com/syuilo/misskey'
};
},
created() {
this.$root.getMeta().then(meta => {
if (meta.maintainer)
this.repositoryUrl = meta.maintainer.repository_url;
});
}
});
</script>
<style lang="stylus" scoped>
.a
display block

View File

@@ -5,17 +5,17 @@
<div style="overflow: hidden; line-height: 28px;">
<p class="turn" v-if="!iAmPlayer && !game.isEnded">
<mfm :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
<mk-ellipsis/>
</p>
<p class="turn" v-if="logPos != logs.length">
<mfm :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
</p>
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
<p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner">
<mfm :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/>
<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/>
<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
</template>
<template v-else>{{ $t('@.reversi.drawn') }}</template>

View File

@@ -12,21 +12,54 @@
</li>
</ul>
<button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button>
<button class="add" v-else disabled>{{ $t('no-more') }}</button>
<button class="destroy" @click="destroy" :title="$t('destroy')">
<fa icon="times"/>
</button>
<section>
<ui-switch v-model="multiple">{{ $t('multiple') }}</ui-switch>
<div>
<ui-select v-model="expiration">
<template #label>{{ $t('expiration') }}</template>
<option value="infinite">{{ $t('infinite') }}</option>
<option value="at">{{ $t('at') }}</option>
<option value="after">{{ $t('after') }}</option>
</ui-select>
<section v-if="expiration === 'at'">
<ui-input v-model="atDate" type="date">{{ $t('deadline-date') }}</ui-input>
<ui-input v-model="atTime" type="time">{{ $t('deadline-time') }}</ui-input>
</section>
<section v-if="expiration === 'after'">
<ui-input v-model="after" type="number">{{ $t('interval') }}</ui-input>
<ui-select v-model="unit">
<template #label>{{ $t('unit') }}</template>
<option value="second">{{ $t('second') }}</option>
<option value="minute">{{ $t('minute') }}</option>
<option value="hour">{{ $t('hour') }}</option>
<option value="day">{{ $t('day') }}</option>
</ui-select>
</section>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as moment from 'moment';
import i18n from '../../../i18n';
import { erase } from '../../../../../prelude/array';
export default Vue.extend({
i18n: i18n('common/views/components/poll-editor.vue'),
data() {
return {
choices: ['', '']
choices: ['', ''],
multiple: false,
expiration: 'infinite',
atDate: moment().add(1, 'day').toISOString().split('T')[0],
atTime: '00:00',
after: 0,
unit: 'second'
};
},
watch: {
@@ -55,15 +88,46 @@ export default Vue.extend({
},
get() {
const at = () => {
const [date] = moment(this.atDate).toISOString().split('T');
const [hour, minute] = this.atTime.split(':');
return moment(`${date}T${hour}:${minute}Z`).valueOf();
};
const after = () => {
let base = parseInt(this.after);
switch (this.unit) {
case 'day': base *= 24;
case 'hour': base *= 60;
case 'minute': base *= 60;
case 'second': return base *= 1000;
default: return null;
}
};
return {
choices: erase('', this.choices)
}
choices: erase('', this.choices),
multiple: this.multiple,
...(
this.expiration === 'at' ? { expiresAt: at() } :
this.expiration === 'after' ? { expiredAfter: after() } : {})
};
},
set(data) {
if (data.choices.length == 0) return;
this.choices = data.choices;
if (data.choices.length == 1) this.choices = this.choices.concat('');
this.multiple = data.multiple;
if (data.expiresAt) {
this.expiration = 'at';
this.atDate = this.atTime = data.expiresAt;
} else if (typeof data.expiredAfter === 'number') {
this.expiration = 'after';
this.after = data.expiredAfter;
} else {
this.expiration = 'infinite';
}
}
}
});
@@ -128,6 +192,7 @@ export default Vue.extend({
margin 8px 0 0 0
vertical-align top
color var(--primary)
z-index 1
> .destroy
position absolute
@@ -142,4 +207,23 @@ export default Vue.extend({
&:active
color var(--primaryDarken30)
> section
margin 16px 0 -16px 0
> div
margin 0 8px
&:last-child
flex 1 0 auto
> section
align-items center
display flex
margin -32px 0 0
> :first-child
margin-right 16px
> .ui-input
flex 1 0 auto
</style>

View File

@@ -1,8 +1,8 @@
<template>
<div class="mk-poll" :data-is-voted="isVoted">
<div class="mk-poll" :data-done="closed || isVoted">
<ul>
<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span>
<template v-if="choice.isVoted"><fa icon="check"/></template>
<mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/>
@@ -10,11 +10,13 @@
</span>
</li>
</ul>
<p v-if="total > 0">
<span>{{ $t('total-users').replace('{}', total) }}</span>
<span></span>
<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
<p>
<span>{{ $t('total-votes').replace('{}', total) }}</span>
<span> · </span>
<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
<span v-if="isVoted">{{ $t('voted') }}</span>
<span v-else-if="closed">{{ $t('closed') }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span>
</p>
</div>
</template>
@@ -28,6 +30,7 @@ export default Vue.extend({
props: ['note'],
data() {
return {
remaining: -1,
showResult: false
};
},
@@ -38,19 +41,43 @@ export default Vue.extend({
total(): number {
return sum(this.poll.choices.map(x => x.votes));
},
closed(): boolean {
return !this.remaining;
},
timer(): string {
return this.$t(
this.remaining > 86400 ? 'remaining-days' :
this.remaining > 3600 ? 'remaining-hours' :
this.remaining > 60 ? 'remaining-minutes' : 'remaining-seconds')
.replace('{s}', Math.floor(this.remaining % 60))
.replace('{m}', Math.floor(this.remaining / 60) % 60)
.replace('{h}', Math.floor(this.remaining / 3600) % 24)
.replace('{d}', Math.floor(this.remaining / 86400));
},
isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted);
return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
}
},
created() {
this.showResult = this.isVoted;
if (this.note.poll.expiresAt) {
const update = () => {
if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
requestAnimationFrame(update);
else
this.showResult = true;
};
update();
}
},
methods: {
toggleShowResult() {
this.showResult = !this.showResult;
},
vote(id) {
if (this.poll.choices.some(c => c.isVoted)) return;
if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
this.$root.api('notes/polls/vote', {
noteId: this.note.id,
choice: id
@@ -61,7 +88,7 @@ export default Vue.extend({
Vue.set(c, 'isVoted', true);
}
}
this.showResult = true;
if (!this.showResult) this.showResult = !this.poll.multiple;
});
}
}
@@ -114,7 +141,7 @@ export default Vue.extend({
a
color inherit
&[data-is-voted]
&[data-done]
> ul > li
cursor default

View File

@@ -97,6 +97,7 @@
<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>
<option value="user-lists">{{ $t('export-targets.user-lists') }}</option>
</ui-select>
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
</div>
@@ -284,6 +285,7 @@ export default Vue.extend({
this.exportTarget == 'following' ? 'i/export-following' :
this.exportTarget == 'mute' ? 'i/export-mute' :
this.exportTarget == 'blocking' ? 'i/export-blocking' :
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
null, {});
this.$root.dialog({

View File

@@ -386,7 +386,7 @@ export default Vue.extend({
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
&:before {
content: 'Light';
@@ -418,14 +418,14 @@ export default Vue.extend({
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
}
@@ -454,7 +454,7 @@ export default Vue.extend({
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
@@ -486,7 +486,7 @@ export default Vue.extend({
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
@@ -559,13 +559,13 @@ export default Vue.extend({
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}

View File

@@ -366,6 +366,9 @@ root(fill)
&[type='file']
display none
&[type='number']
text-align right
> .prefix
> .suffix
display block

View File

@@ -6,7 +6,7 @@
<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
<div ref="chart" class="chart"></div>
<x-hashtag-tl :tag-tl="tagTl" class="tl"/>
<x-hashtag-tl :tag-tl="tagTl" class="tl" :key="JSON.stringify(tagTl)"/>
</div>
</x-column>
</template>

View File

@@ -26,6 +26,7 @@
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
<option value="server">{{ $t('@.widgets.server') }}</option>
<option value="queue">{{ $t('@.widgets.queue') }}</option>
<option value="nav">{{ $t('@.widgets.nav') }}</option>
<option value="tips">{{ $t('@.widgets.tips') }}</option>
</select>

View File

@@ -31,3 +31,4 @@ Vue.component('mkw-version', wVersion);
Vue.component('mkw-hashtags', wHashtags);
Vue.component('mkw-instance', wInstance);
Vue.component('mkw-post-form', wPostForm);
Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default));

View File

@@ -0,0 +1,157 @@
<template>
<div>
<ui-container :show-header="!props.compact">
<template #header><fa :icon="faTasks"/>Queue</template>
<div class="mntrproz">
<div>
<b>In</b>
<span v-if="latestStats">{{ latestStats.inbox.active | number }} / {{ latestStats.inbox.delayed | number }}</span>
<div ref="in"></div>
</div>
<div>
<b>Out</b>
<span v-if="latestStats">{{ latestStats.deliver.active | number }} / {{ latestStats.deliver.delayed | number }}</span>
<div ref="out"></div>
</div>
</div>
</ui-container>
</div>
</template>
<script lang="ts">
import define from '../../define-widget';
import { faTasks } from '@fortawesome/free-solid-svg-icons';
import ApexCharts from 'apexcharts';
export default define({
name: 'queue',
props: () => ({
compact: false
})
}).extend({
data() {
return {
stats: [],
inChart: null,
outChart: null,
faTasks
};
},
watch: {
stats(stats) {
this.inChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
computed: {
latestStats(): any {
return this.stats[this.stats.length - 1];
}
},
mounted() {
const chartOpts = {
chart: {
type: 'area',
height: 70,
animations: {
dynamicAnimation: {
enabled: false
}
},
sparkline: {
enabled: true,
}
},
tooltip: {
enabled: false
},
stroke: {
curve: 'straight',
width: 1
},
series: [{
data: [] as any
}, {
data: [] as any
}],
yaxis: {
min: 0,
}
};
this.inChart = new ApexCharts(this.$refs.in, chartOpts);
this.outChart = new ApexCharts(this.$refs.out, chartOpts);
this.inChart.render();
this.outChart.render();
const connection = this.$root.stream.useSharedConnection('queueStats');
connection.on('stats', this.onStats);
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 50
});
this.$once('hook:beforeDestroy', () => {
connection.dispose();
this.inChart.destroy();
this.outChart.destroy();
});
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="stylus" scoped>
.mntrproz
display flex
padding 4px
> div
width 50%
padding 4px
> b
display block
font-size 12px
color var(--text)
> span
position absolute
top 4px
right 4px
opacity 0.7
font-size 12px
color var(--text)
</style>

View File

@@ -129,9 +129,9 @@ export default Vue.extend({
mounted() {
// Get replies
if (!this.compact) {
this.$root.api('notes/replies', {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
limit: 8
limit: 30
}).then(replies => {
this.replies = replies;
});

View File

@@ -123,9 +123,9 @@ export default Vue.extend({
created() {
if (this.detail) {
this.$root.api('notes/replies', {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
limit: 8
limit: 30
}).then(replies => {
this.replies = replies;
});

View File

@@ -115,6 +115,8 @@ export default Vue.extend({
uploadings: [],
poll: false,
pollChoices: [],
pollMultiple: false,
pollExpiration: [],
useCw: false,
cw: null,
geo: null,
@@ -295,7 +297,10 @@ export default Vue.extend({
},
onPollUpdate() {
this.pollChoices = this.$refs.poll.get().choices;
const got = this.$refs.poll.get();
this.pollChoices = got.choices;
this.pollMultiple = got.multiple;
this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter];
this.saveDraft();
},

View File

@@ -27,6 +27,7 @@
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
<option value="server">{{ $t('@.widgets.server') }}</option>
<option value="queue">{{ $t('@.widgets.queue') }}</option>
<option value="nav">{{ $t('@.widgets.nav') }}</option>
<option value="tips">{{ $t('@.widgets.tips') }}</option>
</select>

View File

@@ -172,7 +172,11 @@ export default class MiOS extends EventEmitter {
callback();
// Init service worker
if (this.shouldRegisterSw) this.registerSw();
if (this.shouldRegisterSw) {
this.getMeta().then(data => {
this.registerSw(data.swPublickey);
});
}
};
// キャッシュがあったとき
@@ -302,7 +306,7 @@ export default class MiOS extends EventEmitter {
* Register service worker
*/
@autobind
private registerSw() {
private registerSw(swPublickey) {
// Check whether service worker and push manager supported
const isSwSupported =
('serviceWorker' in navigator) && ('PushManager' in window);
@@ -328,7 +332,7 @@ export default class MiOS extends EventEmitter {
// A public key your push server will use to send
// messages to client apps via a push server.
applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey)
applicationServerKey: urlBase64ToUint8Array(swPublickey)
};
// Subscribe push notification

View File

@@ -135,9 +135,9 @@ export default Vue.extend({
methods: {
fetchReplies() {
if (this.compact) return;
this.$root.api('notes/replies', {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
limit: 8
limit: 30
}).then(replies => {
this.replies = replies;
});

View File

@@ -115,9 +115,9 @@ export default Vue.extend({
created() {
if (this.detail) {
this.$root.api('notes/replies', {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
limit: 8
limit: 30
}).then(replies => {
this.replies = replies;
});

View File

@@ -105,6 +105,7 @@ export default Vue.extend({
files: [],
poll: false,
pollChoices: [],
pollMultiple: false,
geo: null,
visibility: 'public',
visibleUsers: [],
@@ -273,7 +274,9 @@ export default Vue.extend({
},
onPollUpdate() {
this.pollChoices = this.$refs.poll.get().choices;
const got = this.$refs.poll.get();
this.pollChoices = got.choices;
this.pollMultiple = got.multiple;
},
upload(file) {

View File

@@ -19,6 +19,7 @@
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
<option value="version">{{ $t('@.widgets.version') }}</option>
<option value="server">{{ $t('@.widgets.server') }}</option>
<option value="queue">{{ $t('@.widgets.queue') }}</option>
<option value="memo">{{ $t('@.widgets.memo') }}</option>
<option value="nav">{{ $t('@.widgets.nav') }}</option>
<option value="tips">{{ $t('@.widgets.tips') }}</option>

View File

@@ -43,11 +43,11 @@ export const builtinThemes = [
];
export function applyTheme(theme: Theme, persisted = true) {
document.documentElement.classList.add('change-theme');
document.documentElement.classList.add('changing-theme');
setTimeout(() => {
document.documentElement.classList.remove('change-theme');
}, 500);
document.documentElement.classList.remove('changing-theme');
}, 1000);
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));

View File

@@ -20,11 +20,9 @@ html, body
text-size-adjust 100%
font-family sans-serif
html.change-theme
html.changing-theme
&, *
transition-property all
transition-duration 0.5s
transition-timing-function ease
transition background 1s ease !important
a
text-decoration none

View File

@@ -19,6 +19,8 @@ export type Source = {
host: string;
port: number;
pass: string;
db?: number;
prefix?: string;
};
elasticsearch: {
host: string;

View File

@@ -0,0 +1,37 @@
import * as Deque from 'double-ended-queue';
import Xev from 'xev';
import { deliverQueue, inboxQueue } from '../queue';
const ev = new Xev();
const interval = 1000;
/**
* Report queue stats regularly
*/
export default function() {
const log = new Deque<any>();
ev.on('requestQueueStatsLog', x => {
ev.emit(`queueStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50));
});
async function tick() {
const deliverJobCounts = await deliverQueue.getJobCounts();
const inboxJobCounts = await inboxQueue.getJobCounts();
const stats = {
deliver: deliverJobCounts,
inbox: inboxJobCounts
};
ev.emit('queueStats', stats);
log.unshift(stats);
if (log.length > 200) log.pop();
}
tick();
setInterval(tick, interval);
}

View File

@@ -5,6 +5,8 @@ export default config.redis ? redis.createClient(
config.redis.port,
config.redis.host,
{
auth_pass: config.redis.pass
auth_pass: config.redis.pass,
prefix: config.redis.prefix,
db: config.redis.db || 0
}
) : null;

View File

@@ -16,6 +16,7 @@ import Xev from 'xev';
import Logger from './services/logger';
import serverStats from './daemons/server-stats';
import notesStats from './daemons/notes-stats';
import queueStats from './daemons/queue-stats';
import loadConfig from './config/load';
import { Config } from './config/types';
import { lessThan } from './prelude/array';
@@ -50,6 +51,7 @@ function main() {
if (program.daemons) {
serverStats();
notesStats();
queueStats();
}
}
@@ -73,7 +75,7 @@ function greet() {
console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
console.log('');
console.log(chalk`<${os.hostname()} {gray (PID: ${process.pid.toString()})}>`);
console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
}
bootLogger.info('Welcome to Misskey!');
@@ -117,9 +119,6 @@ async function masterMain() {
await spawnWorkers(config.clusterLimit);
}
// start queue
require('./queue').default();
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
}
@@ -130,6 +129,9 @@ async function workerMain() {
// start server
await require('./server').default();
// start job queue
require('./queue').default();
if (cluster.isWorker) {
// Send a 'ready' message to parent process
process.send('ready');
@@ -150,13 +152,9 @@ async function queueMain() {
bootLogger.succ('Misskey initialized');
// start processor
const queue = require('./queue').default();
require('./queue').default();
if (queue) {
bootLogger.succ('Queue started', null, true);
} else {
bootLogger.error('Queue not available');
}
bootLogger.succ('Queue started', null, true);
}
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));

View File

@@ -142,7 +142,7 @@ export const mfmLanguage = P.createLanguage({
},
hashtag: () => P((input, 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');
let hashtag = match[1];
hashtag = removeOrphanedBrackets(hashtag);

View File

@@ -4,6 +4,7 @@ import AccessToken from './access-token';
import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid';
import config from '../config';
import { dbLogger } from '../db/logger';
const App = db.get<IApp>('apps');
App.createIndex('secret');
@@ -66,6 +67,12 @@ export const pack = (
}
}
// (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
if (_app == null) {
dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
return null;
}
// Rename _id to id
_app.id = _app._id;
delete _app._id;

View File

@@ -3,6 +3,8 @@ import db from '../db/mongodb';
const Log = db.get<ILog>('logs');
Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
Log.createIndex('level');
Log.createIndex('domain');
export default Log;
export interface ILog {

View File

@@ -19,6 +19,7 @@ Note.createIndex('userId');
Note.createIndex('mentions');
Note.createIndex('visibleUserIds');
Note.createIndex('replyId');
Note.createIndex('renoteId');
Note.createIndex('tagsLower');
Note.createIndex('_user.host');
Note.createIndex('_files._id');
@@ -35,6 +36,7 @@ export type INote = {
_id: mongo.ObjectID;
createdAt: Date;
deletedAt: Date;
updatedAt?: Date;
fileIds: mongo.ObjectID[];
replyId: mongo.ObjectID;
renoteId: mongo.ObjectID;
@@ -99,7 +101,9 @@ export type INote = {
};
export type IPoll = {
choices: IChoice[]
choices: IChoice[];
multiple?: boolean;
expiresAt?: Date;
};
export type IChoice = {
@@ -313,15 +317,31 @@ export const pack = async (
// Poll
if (meId && _note.poll) {
_note.poll = (async poll => {
if (poll.multiple) {
const votes = await PollVote.find({
userId: meId,
noteId: id
});
const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
for (const myChoice of myChoices) {
(myChoice as any).isVoted = true;
}
return poll;
} else {
poll.multiple = false;
}
const vote = await PollVote
.findOne({
userId: meId,
noteId: id
});
if (vote != null) {
const myChoice = poll.choices
.filter((c: any) => c.id == vote.choice)[0];
if (vote) {
const myChoice = (poll.choices as IChoice[])
.filter(x => x.id == vote.choice)[0] as any;
myChoice.isVoted = true;
}

View File

@@ -2,9 +2,10 @@ import * as mongo from 'mongodb';
import db from '../db/mongodb';
const PollVote = db.get<IPollVote>('pollVotes');
PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
PollVote.createIndex('userId');
PollVote.createIndex('noteId');
PollVote.createIndex(['userId', 'noteId'], { unique: true });
PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
export default PollVote;
export interface IPollVote {

View File

@@ -1,164 +1,166 @@
import * as Queue from 'bee-queue';
import * as Queue from 'bull';
import * as httpSignature from 'http-signature';
import config from '../config';
import { ILocalUser } from '../models/user';
import { program } from '../argv';
import handler from './processors';
import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
import processDb from './processors/db';
import { queueLogger } from './logger';
const enableQueue = !program.disableQueue;
const enableQueueProcessing = !program.onlyServer && enableQueue;
const queueAvailable = config.redis != null;
const queue = initializeQueue();
function initializeQueue() {
if (queueAvailable && enableQueue) {
return new Queue('misskey-queue', {
redis: {
port: config.redis.port,
host: config.redis.host,
password: config.redis.pass
},
removeOnSuccess: true,
removeOnFailure: true,
getEvents: false,
sendEvents: false,
storeJobs: false
});
} else {
return null;
}
function initializeQueue(name: string) {
return new Queue(name, config.redis != null ? {
redis: {
port: config.redis.port,
host: config.redis.host,
password: config.redis.pass,
db: config.redis.db || 0,
},
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue'
} : null);
}
export const deliverQueue = initializeQueue('deliver');
export const inboxQueue = initializeQueue('inbox');
export const dbQueue = initializeQueue('db');
const deliverLogger = queueLogger.createSubLogger('deliver');
const inboxLogger = queueLogger.createSubLogger('inbox');
deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.debug(`failed(${err}) id=${job.id} to=${job.data.to}`))
.on('error', (error) => deliverLogger.error(`error ${error}`))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => inboxLogger.debug(`failed(${err}) id=${job.id}`))
.on('error', (error) => inboxLogger.error(`error ${error}`))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id}`));
export function deliver(user: ILocalUser, content: any, to: any) {
if (content == null) return;
if (content == null) return null;
const data = {
type: 'deliver',
user,
content,
to
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data)
.retries(8)
.backoff('exponential', 1000)
.save();
} else {
return handler({ data }, () => {});
}
return deliverQueue.add(data, {
attempts: 8,
backoff: {
type: 'exponential',
delay: 60 * 1000
},
removeOnComplete: true,
removeOnFail: true
});
}
export function processInbox(activity: any, signature: httpSignature.IParsedSignature) {
export function inbox(activity: any, signature: httpSignature.IParsedSignature) {
const data = {
type: 'processInbox',
activity: activity,
signature
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data)
.retries(3)
.backoff('exponential', 500)
.save();
} else {
return handler({ data }, () => {});
}
return inboxQueue.add(data, {
attempts: 8,
backoff: {
type: 'exponential',
delay: 1000
},
removeOnComplete: true,
removeOnFail: true
});
}
export function createDeleteNotesJob(user: ILocalUser) {
const data = {
type: 'deleteNotes',
return dbQueue.add('deleteNotes', {
user: user
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createDeleteDriveFilesJob(user: ILocalUser) {
const data = {
type: 'deleteDriveFiles',
return dbQueue.add('deleteDriveFiles', {
user: user
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createExportNotesJob(user: ILocalUser) {
const data = {
type: 'exportNotes',
return dbQueue.add('exportNotes', {
user: user
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createExportFollowingJob(user: ILocalUser) {
const data = {
type: 'exportFollowing',
return dbQueue.add('exportFollowing', {
user: user
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createExportMuteJob(user: ILocalUser) {
const data = {
type: 'exportMute',
return dbQueue.add('exportMute', {
user: user
};
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createExportBlockingJob(user: ILocalUser) {
const data = {
type: 'exportBlocking',
return dbQueue.add('exportBlocking', {
user: user
};
}, {
removeOnComplete: true,
removeOnFail: true
});
}
if (queueAvailable && enableQueueProcessing) {
return queue.createJob(data).save();
} else {
return handler({ data }, () => {});
}
export function createExportUserListsJob(user: ILocalUser) {
return dbQueue.add('exportUserLists', {
user: user
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export default function() {
if (queueAvailable && enableQueueProcessing) {
queue.process(128, handler);
queueLogger.succ('Processing started');
if (!program.onlyServer) {
deliverQueue.process(128, processDeliver);
inboxQueue.process(128, processInbox);
processDb(dbQueue);
}
return queue;
}
export function destroy() {
queue.destroy().then(n => {
queueLogger.succ(`All job removed (${n} jobs)`);
deliverQueue.once('cleaned', (jobs, status) => {
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
deliverQueue.clean(0, 'wait');
inboxQueue.once('cleaned', (jobs, status) => {
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
inboxQueue.clean(0, 'wait');
}

View File

@@ -1,14 +1,14 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
import User from '../../models/user';
import DriveFile from '../../models/drive-file';
import deleteFile from '../../services/drive/delete-file';
import { queueLogger } from '../../logger';
import User from '../../../models/user';
import DriveFile from '../../../models/drive-file';
import deleteFile from '../../../services/drive/delete-file';
const logger = queueLogger.createSubLogger('delete-drive-files');
export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> {
logger.info(`Deleting drive files of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -32,7 +32,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
if (files.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -47,7 +47,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
userId: user._id,
});
if (job.reportProgress) job.reportProgress(deletedCount / total);
job.progress(deletedCount / total);
}
logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);

View File

@@ -1,14 +1,14 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
import Note from '../../models/note';
import deleteNote from '../../services/note/delete';
import User from '../../models/user';
import { queueLogger } from '../../logger';
import Note from '../../../models/note';
import deleteNote from '../../../services/note/delete';
import User from '../../../models/user';
const logger = queueLogger.createSubLogger('delete-notes');
export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
export async function deleteNotes(job: Bull.Job, done: any): Promise<void> {
logger.info(`Deleting notes of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -32,7 +32,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
if (notes.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -47,7 +47,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
userId: user._id,
});
if (job.reportProgress) job.reportProgress(deletedCount / total);
job.progress(deletedCount / total);
}
logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);

View File

@@ -1,18 +1,18 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
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 { 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';
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> {
export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
logger.info(`Exporting blocking of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -48,7 +48,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
if (blockings.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -74,7 +74,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
blockerId: user._id,
});
if (job.reportProgress) job.reportProgress(exportedCount / total);
job.progress(exportedCount / total);
}
stream.end();

View File

@@ -1,18 +1,18 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
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 { 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';
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> {
export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
logger.info(`Exporting following of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -48,7 +48,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
if (followings.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -74,7 +74,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
followerId: user._id,
});
if (job.reportProgress) job.reportProgress(exportedCount / total);
job.progress(exportedCount / total);
}
stream.end();

View File

@@ -1,18 +1,18 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
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 { 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';
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> {
export async function exportMute(job: Bull.Job, done: any): Promise<void> {
logger.info(`Exporting mute of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -48,7 +48,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
if (mutes.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -74,7 +74,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
muterId: user._id,
});
if (job.reportProgress) job.reportProgress(exportedCount / total);
job.progress(exportedCount / total);
}
stream.end();

View File

@@ -1,17 +1,17 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as mongo from 'mongodb';
import { queueLogger } from '../logger';
import Note, { INote } from '../../models/note';
import addFile from '../../services/drive/add-file';
import User from '../../models/user';
import { queueLogger } from '../../logger';
import Note, { INote } from '../../../models/note';
import addFile from '../../../services/drive/add-file';
import User from '../../../models/user';
import dateFormat = require('dateformat');
const logger = queueLogger.createSubLogger('export-notes');
export async function exportNotes(job: bq.Job, done: any): Promise<void> {
export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
logger.info(`Exporting notes of ${job.data.user._id} ...`);
const user = await User.findOne({
@@ -58,7 +58,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
if (notes.length === 0) {
ended = true;
if (job.reportProgress) job.reportProgress(100);
job.progress(100);
break;
}
@@ -83,7 +83,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
userId: user._id,
});
if (job.reportProgress) job.reportProgress(exportedNotesCount / total);
job.progress(exportedNotesCount / total);
}
await new Promise((res, rej) => {

View File

@@ -0,0 +1,73 @@
import * as Bull from 'bull';
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 config from '../../../config';
import UserList from '../../../models/user-list';
const logger = queueLogger.createSubLogger('export-user-lists');
export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
logger.info(`Exporting user lists of ${job.data.user._id} ...`);
const user = await User.findOne({
_id: new mongo.ObjectID(job.data.user._id.toString())
});
const lists = await UserList.find({
userId: user._id
});
// 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' });
for (const list of lists) {
const users = await User.find({
_id: { $in: list.userIds }
}, {
fields: {
username: true,
host: true
}
});
for (const u of users) {
const acct = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
const content = `${list.title},${acct}`;
await new Promise((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
}
}
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + 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

@@ -1,31 +1,24 @@
import deliver from './http/deliver';
import processInbox from './http/process-inbox';
import * as Bull from 'bull';
import { deleteNotes } from './delete-notes';
import { deleteDriveFiles } from './delete-drive-files';
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 { exportUserLists } from './export-user-lists';
const handlers: any = {
deliver,
processInbox,
const jobs = {
deleteNotes,
deleteDriveFiles,
exportNotes,
exportFollowing,
exportMute,
exportBlocking,
};
exportUserLists
} as any;
export default (job: any, done: any) => {
const handler = handlers[job.data.type];
if (handler) {
handler(job, done);
} else {
queueLogger.error(`Unknown job: ${job.data.type}`);
done();
export default function(dbQueue: Bull.Queue) {
for (const [k, v] of Object.entries(jobs)) {
dbQueue.process(k, v as any);
}
};
}

View File

@@ -1,15 +1,22 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
import request from '../../remote/activitypub/request';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import instanceChart from '../../services/chart/instance';
import Logger from '../../services/logger';
import request from '../../../remote/activitypub/request';
import { queueLogger } from '../../logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import instanceChart from '../../../services/chart/instance';
const logger = new Logger('deliver');
export default async (job: bq.Job, done: any): Promise<void> => {
let latest: string = null;
export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to);
try {
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
logger.debug(`delivering ${latest}`);
}
await request(job.data.user, job.data.to, job.data.content);
// Update stats
@@ -26,7 +33,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
instanceChart.requestSent(i.host, true);
});
done();
return 'Success';
} catch (res) {
// Update stats
registerOrFetchInstanceDoc(host).then(i => {
@@ -42,18 +49,21 @@ export default async (job: bq.Job, done: any): Promise<void> => {
});
if (res != null && res.hasOwnProperty('statusCode')) {
queueLogger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
logger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
// 4xx
if (res.statusCode >= 400 && res.statusCode < 500) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく
done();
} else {
done(res.statusMessage);
return `${res.statusCode} ${res.statusMessage}`;
}
// 5xx etc.
throw `${res.statusCode} ${res.statusMessage}`;
} else {
queueLogger.warn(`deliver failed: ${res} to=${job.data.to}`);
done();
// DNS error, socket error, timeout ...
logger.warn(`deliver failed: ${res} to=${job.data.to}`);
throw res;
}
}
};

View File

@@ -1,21 +1,21 @@
import * as bq from 'bee-queue';
import * as Bull from 'bull';
import * as httpSignature from 'http-signature';
import parseAcct from '../../../misc/acct/parse';
import User, { IRemoteUser } from '../../../models/user';
import perform from '../../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person';
import parseAcct from '../../misc/acct/parse';
import User, { IRemoteUser } from '../../models/user';
import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
import { toUnicode } from 'punycode';
import { URL } from 'url';
import { publishApLogStream } from '../../../services/stream';
import Logger from '../../../services/logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import instanceChart from '../../../services/chart/instance';
import { publishApLogStream } from '../../services/stream';
import Logger from '../../services/logger';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
import Instance from '../../models/instance';
import instanceChart from '../../services/chart/instance';
const logger = new Logger('inbox');
// ユーザーのinboxにアクティビティが届いた時の処理
export default async (job: bq.Job, done: any): Promise<void> => {
export default async (job: Bull.Job): Promise<void> => {
const signature = job.data.signature;
const activity = job.data.activity;
@@ -33,7 +33,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
if (host === null) {
logger.warn(`request was made by local user: @${username}`);
done();
return;
}
@@ -42,7 +41,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
ValidateActivity(activity, host);
} catch (e) {
logger.warn(e.message);
done();
return;
}
@@ -50,8 +48,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
logger.info(`Blocked request: ${host}`);
return;
}
@@ -63,7 +60,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
ValidateActivity(activity, host);
} catch (e) {
logger.warn(e.message);
done();
return;
}
@@ -72,7 +68,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`);
done();
return;
}
@@ -82,7 +77,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}) as IRemoteUser;
}
// Update activityの場合は、ここで署名検証/更新処理まで実施して終了
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') {
if (user == null) {
@@ -92,9 +87,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
} else {
updatePerson(activity.actor, null, activity.object);
}
return;
}
done();
return;
}
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
@@ -103,13 +97,11 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}
if (user === null) {
done(new Error('failed to resolve user'));
return;
throw new Error('failed to resolve user');
}
if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
logger.error('signature verification failed');
done();
return;
}
@@ -136,12 +128,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
});
// アクティビティを処理
try {
await perform(user, activity);
done();
} catch (e) {
done(e);
}
await perform(user, activity);
};
/**

View File

@@ -27,6 +27,10 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
announceNote(resolver, actor, activity, object as INote);
break;
case 'Question':
announceNote(resolver, actor, activity, object as INote);
break;
default:
logger.warn(`Unknown announce type: ${object.type}`);
break;

View File

@@ -29,7 +29,19 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
return;
}
const renote = await resolveNote(note);
// Announce対象をresolve
let renote;
try {
renote = await resolveNote(note);
} catch (e) {
// 対象が4xxならスキップ
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`);
return;
}
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
}
logger.info(`Creating the (Re)Note: ${uri}`);

View File

@@ -1,7 +1,7 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import createNote from './note';
import createImage from './image';
import createNote from './note';
import { ICreate } from '../../type';
import { apLogger } from '../../logger';
@@ -32,6 +32,10 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
createNote(resolver, actor, object);
break;
case 'Question':
createNote(resolver, actor, object);
break;
default:
logger.warn(`Unknown type: ${object.type}`);
break;

View File

@@ -24,6 +24,10 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
deleteNote(actor, uri);
break;
case 'Question':
deleteNote(actor, uri);
break;
case 'Tombstone':
const note = await Note.findOne({ uri });
if (note != null) {

View File

@@ -2,6 +2,7 @@ import { Object } from '../type';
import { IRemoteUser } from '../../../models/user';
import create from './create';
import performDeleteActivity from './delete';
import performUpdateActivity from './update';
import follow from './follow';
import undo from './undo';
import like from './like';
@@ -23,6 +24,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await performDeleteActivity(actor, activity);
break;
case 'Update':
await performUpdateActivity(actor, activity);
break;
case 'Follow':
await follow(actor, activity);
break;

View File

@@ -0,0 +1,28 @@
import { IRemoteUser } from '../../../../models/user';
import { IUpdate, IObject } from '../../type';
import { apLogger } from '../../logger';
import { updateQuestion } from '../../models/question';
/**
* Updateアクティビティを捌きます
*/
export default async (actor: IRemoteUser, activity: IUpdate): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
apLogger.debug('Update');
const object = activity.object as IObject;
switch (object.type) {
case 'Question':
apLogger.debug('Question');
await updateQuestion(object).catch(e => console.log(e));
break;
default:
apLogger.warn(`Unknown type: ${object.type}`);
break;
}
};

View File

@@ -27,7 +27,17 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
const instance = await fetchMeta();
const cache = instance.cacheRemoteFiles;
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
let file;
try {
file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
} catch (e) {
// 4xxの場合は添付されてなかったことにする
if (e >= 400 && e < 500) {
logger.warn(`Ignored image: ${image.url} - ${e}`);
return null;
}
throw e;
}
if (file.metadata.isRemote) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、

View File

@@ -18,6 +18,7 @@ import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
import { IDriveFile } from '../../../models/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
const logger = apLogger;
@@ -52,15 +53,23 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
if (resolver == null) resolver = new Resolver();
const object = await resolver.resolve(value) as any;
const object: any = await resolver.resolve(value);
if (object == null || object.type !== 'Note') {
logger.error(`invalid note: ${object}`);
if (!object || !['Note', 'Question'].includes(object.type)) {
logger.error(`invalid note: ${value}`, {
resolver: {
history: resolver.getHistory()
},
value: value,
object: object
});
return null;
}
const note: INoteActivityStreamsObject = object;
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
@@ -72,6 +81,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}
//#region Visibility
note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to;
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
let visibility = 'public';
let visibleUsers: IUser[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
@@ -83,7 +95,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'specified';
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
}
}
}
//#endergion
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
@@ -95,13 +107,26 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// TODO: attachmentは必ずしも配列ではない
// Noteがsensitiveなら添付もsensitiveにする
const limit = promiseLimit(2);
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
? await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
.filter(image => image != null)
: [];
// リプライ
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
const reply = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => {
// 4xxの場合はリプライしてないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `);
return null;
}
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
})
: null;
// 引用
let quote: INote;
@@ -113,15 +138,34 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
const text = note._misskey_content ? note._misskey_content : fromHtml(note.content);
const text = note._misskey_content || fromHtml(note.content);
// vote
if (reply && reply.poll && text != null) {
const m = text.match(/([0-9])$/);
if (m) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`);
await vote(actor, reply, Number(m[1]));
if (reply && reply.poll) {
const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await vote(actor, reply, index);
// リモートフォロワーにUpdate配信
deliverQuestionUpdate(reply._id);
}
return null;
};
if (note.name) {
return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
}
// 後方互換性のため
if (text) {
const m = text.match(/(\d+)$/);
if (m) {
return await tryCreateVote(m[0], Number(m[1]));
}
}
}
@@ -133,7 +177,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const apEmojis = emojis.map(emoji => emoji.name);
const questionUri = note._misskey_question;
const poll = questionUri ? await extractPollFromQuestion(questionUri).catch(() => undefined) : undefined;
const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
@@ -142,11 +186,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
return await post(actor, {
createdAt: new Date(note.published),
files: files,
files,
reply,
renote: quote,
cw: cw,
text: text,
cw,
text,
viaMobile: false,
localOnly: false,
geo: undefined,

View File

@@ -1,19 +1,79 @@
import { IChoice, IPoll } from '../../../models/note';
import config from '../../../config';
import Note, { IChoice, IPoll } from '../../../models/note';
import Resolver from '../resolver';
import { IQuestion } from '../type';
import { apLogger } from '../logger';
export async function extractPollFromQuestion(questionUri: string): Promise<IPoll> {
const resolver = new Resolver();
const question = await resolver.resolve(questionUri) as any;
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : null;
const choices: IChoice[] = question.oneOf.map((x: any, i: number) => {
return {
id: i,
text: x.name,
votes: x._misskey_votes || 0,
} as IChoice;
});
if (multiple && !question.anyOf) {
throw 'invalid question';
}
const choices = question[multiple ? 'anyOf' : 'oneOf']
.map((x, i) => ({
id: i,
text: x.name,
votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
} as IChoice));
return {
choices
choices,
multiple,
expiresAt
};
}
/**
* Update votes of Question
* @param uri URI of AP Question object
* @returns true if updated
*/
export async function updateQuestion(value: any) {
const uri = typeof value == 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(config.url + '/')) throw 'uri points local';
//#region このサーバーに既に登録されているか
const note = await Note.findOne({ uri });
if (note == null) throw 'Question is not registed';
//#endregion
// resolve new Question object
const resolver = new Resolver();
const question = await resolver.resolve(value) as IQuestion;
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
if (question.type !== 'Question') throw 'object is not a Question';
const apChoices = question.oneOf || question.anyOf;
const dbChoices = note.poll.choices;
let changed = false;
for (const db of dbChoices) {
const oldCount = db.votes;
const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems;
if (oldCount != newCount) {
changed = true;
db.votes = newCount;
}
}
await Note.update({
_id: note._id
}, {
$set: {
'poll.choices': dbChoices,
updatedAt: new Date(),
}
});
return changed;
}

View File

@@ -15,9 +15,10 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
: Promise.resolve([]);
let inReplyTo;
let inReplyToNote: INote;
if (note.replyId) {
const inReplyToNote = await Note.findOne({
inReplyToNote = await Note.findOne({
_id: note.replyId,
});
@@ -134,6 +135,29 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
...apemojis,
];
const {
choices = [],
expiresAt = null,
multiple = false
} = note.poll || {};
const asPoll = note.poll ? {
type: 'Question',
content: toHtml(Object.assign({}, note, {
text: text
})),
_misskey_fallback_content: content,
[expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt,
[multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({
type: 'Note',
name: text,
replies: {
type: 'Collection',
totalItems: votes
}
}))
} : {};
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
@@ -149,7 +173,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
inReplyTo,
attachment: files.map(renderDocument),
sensitive: files.some(file => file.metadata.isSensitive),
tag
tag,
...asPoll
};
}

View File

@@ -3,17 +3,19 @@ import { ILocalUser } from '../../../models/user';
import { INote } from '../../../models/note';
export default async function renderQuestion(user: ILocalUser, note: INote) {
const question = {
const question = {
type: 'Question',
id: `${config.url}/questions/${note._id}`,
actor: `${config.url}/users/${user._id}`,
content: note.text != null ? note.text : '',
oneOf: note.poll.choices.map(c => {
return {
name: c.text,
_misskey_votes: c.votes,
};
}),
content: note.text || '',
[note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({
name: c.text,
_misskey_votes: c.votes,
replies: {
type: 'Collection',
totalItems: c.votes
}
}))
};
return question;

View File

@@ -0,0 +1,22 @@
import config from '../../../config';
import { INote } from '../../../models/note';
import { IRemoteUser, ILocalUser } from '../../../models/user';
import { IPollVote } from '../../../models/poll-vote';
export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> {
return {
id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`,
actor: `${config.url}/users/${user._id}`,
type: 'Create',
to: [pollOwner.uri],
published: new Date().toISOString(),
object: {
id: `${config.url}/users/${user._id}#votes/${vote._id}`,
type: 'Note',
attributedTo: `${config.url}/users/${user._id}`,
to: [pollOwner.uri],
inReplyTo: pollNote.uri,
name: pollNote.poll.choices.find(x => x.id === vote.choice).text
}
};
}

View File

@@ -14,7 +14,7 @@ import Instance from '../../models/instance';
export const logger = apLogger.createSubLogger('deliver');
export default (user: ILocalUser, url: string, object: any) => new Promise(async (resolve, reject) => {
export default async (user: ILocalUser, url: string, object: any) => {
logger.info(`--> ${url}`);
const timeout = 10 * 1000;
@@ -32,53 +32,57 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
sha256.update(data);
const hash = sha256.digest('base64');
const addr = await resolveAddr(hostname).catch(e => reject(e));
const addr = await resolveAddr(hostname);
if (!addr) return;
const req = request({
protocol,
hostname: addr,
setHost: false,
port,
method: 'POST',
path: pathname + search,
timeout,
headers: {
'Host': host,
'User-Agent': config.userAgent,
'Content-Type': 'application/activity+json',
'Digest': `SHA-256=${hash}`
}
}, res => {
if (res.statusCode >= 400) {
logger.warn(`${url} --> ${res.statusCode}`);
reject(res);
} else {
logger.succ(`${url} --> ${res.statusCode}`);
resolve();
}
const _ = new Promise((resolve, reject) => {
const req = request({
protocol,
hostname: addr,
setHost: false,
port,
method: 'POST',
path: pathname + search,
timeout,
headers: {
'Host': host,
'User-Agent': config.userAgent,
'Content-Type': 'application/activity+json',
'Digest': `SHA-256=${hash}`
}
}, res => {
if (res.statusCode >= 400) {
logger.warn(`${url} --> ${res.statusCode}`);
reject(res);
} else {
logger.succ(`${url} --> ${res.statusCode}`);
resolve();
}
});
sign(req, {
authorizationHeaderName: 'Signature',
key: user.keypair,
keyId: `${config.url}/users/${user._id}/publickey`,
headers: ['date', 'host', 'digest']
});
// Signature: Signature ... => Signature: ...
let sig = req.getHeader('Signature').toString();
sig = sig.replace(/^Signature /, '');
req.setHeader('Signature', sig);
req.on('timeout', () => req.abort());
req.on('error', e => {
if (req.aborted) reject('timeout');
reject(e);
});
req.end(data);
});
sign(req, {
authorizationHeaderName: 'Signature',
key: user.keypair,
keyId: `${config.url}/users/${user._id}/publickey`,
headers: ['date', 'host', 'digest']
});
// Signature: Signature ... => Signature: ...
let sig = req.getHeader('Signature').toString();
sig = sig.replace(/^Signature /, '');
req.setHeader('Signature', sig);
req.on('timeout', () => req.abort());
req.on('error', e => {
if (req.aborted) reject('timeout');
reject(e);
});
req.end(data);
await _;
//#region Log
publishApLogStream({
@@ -88,7 +92,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
actor: user.username
});
//#endregion
});
};
/**
* Resolve host (with cached, asynchrony)

View File

@@ -1,9 +1,6 @@
import * as request from 'request-promise-native';
import { IObject } from './type';
import config from '../../config';
import { apLogger } from './logger';
export const logger = apLogger.createSubLogger('resolver');
export default class Resolver {
private history: Set<string>;
@@ -13,6 +10,10 @@ export default class Resolver {
this.history = new Set();
}
public getHistory(): string[] {
return Array.from(this.history);
}
public async resolveCollection(value: any) {
const collection = typeof value === 'string'
? await this.resolve(value)
@@ -30,7 +31,6 @@ export default class Resolver {
}
default: {
logger.error(`unknown collection type: ${collection.type}`);
throw new Error(`unknown collection type: ${collection.type}`);
}
}
@@ -40,7 +40,6 @@ export default class Resolver {
public async resolve(value: any): Promise<IObject> {
if (value == null) {
logger.error('resolvee is null (or undefined)');
throw new Error('resolvee is null (or undefined)');
}
@@ -49,7 +48,6 @@ export default class Resolver {
}
if (this.history.has(value)) {
logger.error(`cannot resolve already resolved one: ${value}`);
throw new Error('cannot resolve already resolved one');
}
@@ -64,12 +62,6 @@ export default class Resolver {
Accept: 'application/activity+json, application/ld+json'
},
json: true
}).catch(e => {
logger.error(`request error: ${value}: ${e.message}`, {
url: value,
e: e
});
throw new Error(`request error: ${e.message}`);
});
if (object === null || (
@@ -77,10 +69,6 @@ export default class Resolver {
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
)) {
logger.error(`invalid response: ${value}`, {
url: value,
object: object
});
throw new Error('invalid response');
}

View File

@@ -11,7 +11,11 @@ export interface IObject {
attributedTo: string;
attachment?: any[];
inReplyTo?: any;
replies?: ICollection;
content: string;
name?: string;
startTime?: Date;
endTime?: Date;
icon?: any;
image?: any;
url?: string;
@@ -39,12 +43,28 @@ export interface IOrderedCollection extends IObject {
}
export interface INote extends IObject {
type: 'Note';
type: 'Note' | 'Question';
_misskey_content: string;
_misskey_quote: string;
_misskey_question: string;
}
export interface IQuestion extends IObject {
type: 'Note' | 'Question';
_misskey_content: string;
_misskey_quote: string;
_misskey_question: string;
oneOf?: IQuestionChoice[];
anyOf?: IQuestionChoice[];
endTime?: Date;
}
interface IQuestionChoice {
name?: string;
replies?: ICollection;
_misskey_votes?: number;
}
export interface IPerson extends IObject {
type: 'Person';
name: string;
@@ -77,6 +97,10 @@ export interface IDelete extends IActivity {
type: 'Delete';
}
export interface IUpdate extends IActivity {
type: 'Update';
}
export interface IUndo extends IActivity {
type: 'Undo';
}
@@ -119,6 +143,7 @@ export type Object =
IOrderedCollection |
ICreate |
IDelete |
IUpdate |
IUndo |
IFollow |
IAccept |

View File

@@ -16,7 +16,7 @@ import Followers from './activitypub/followers';
import Following from './activitypub/following';
import Featured from './activitypub/featured';
import renderQuestion from '../remote/activitypub/renderer/question';
import { processInbox } from '../queue';
import { inbox as processInbox } from '../queue';
// Init router
const router = new Router();

View File

@@ -1,3 +1,4 @@
import { performance } from 'perf_hooks';
import limiter from './limiter';
import { IUser } from '../../models/user';
import { IApp } from '../../models/app';
@@ -71,6 +72,7 @@ export default async (endpoint: string, user: IUser, app: IApp, data: any, file?
}
// API invoking
const before = performance.now();
return await ep.exec(data, user, app, file).catch((e: Error) => {
if (e instanceof ApiError) {
throw e;
@@ -88,5 +90,11 @@ export default async (endpoint: string, user: IUser, app: IApp, data: any, file?
}
});
}
}).finally(() => {
const after = performance.now();
const time = after - before;
if (time > 1000) {
apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
}
});
};

View File

@@ -0,0 +1,21 @@
import define from '../../../define';
import { deliverQueue, inboxQueue } from '../../../../../queue';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {}
};
export default define(meta, async (ps) => {
const deliverJobCounts = await deliverQueue.getJobCounts();
const inboxJobCounts = await inboxQueue.getJobCounts();
return {
deliver: deliverJobCounts,
inbox: inboxJobCounts
};
});

View File

@@ -97,7 +97,7 @@ async function fetchAny(uri: string) {
};
}
if (object.type === 'Note') {
if (['Note', 'Question'].includes(object.type)) {
const note = await createNote(object.id);
return {
type: 'Note',

View File

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

View File

@@ -0,0 +1,132 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import Note, { packMany } from '../../../../models/note';
import define from '../../define';
import { getFriends } from '../../common/get-friends';
import { getHideUserIds } from '../../common/get-hide-users';
export const meta = {
desc: {
'ja-JP': '指定した投稿への返信/引用を取得します。',
'en-US': 'Get replies/quotes of a note.'
},
tags: ['notes'],
requireCredential: false,
params: {
noteId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象の投稿のID',
'en-US': 'Target note ID'
}
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
transform: transform,
},
untilId: {
validator: $.optional.type(ID),
transform: transform,
},
},
res: {
type: 'array',
items: {
type: 'Note',
},
},
};
export default define(meta, async (ps, user) => {
const [followings, hideUserIds] = await Promise.all([
// フォローを取得
// Fetch following
user ? getFriends(user._id) : [],
// 隠すユーザーを取得
getHideUserIds(user)
]);
const visibleQuery = user == null ? [{
visibility: { $in: [ 'public', 'home' ] }
}] : [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const q = {
$and: [{
$or: [{
replyId: ps.noteId,
}, {
renoteId: ps.noteId,
$or: [{
text: { $ne: null }
}, {
fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
}]
}, {
$or: visibleQuery
}]
} as any;
if (hideUserIds && hideUserIds.length > 0) {
q['userId'] = {
$nin: hideUserIds
};
}
const sort = {
_id: -1
};
if (ps.sinceId) {
sort._id = 1;
q._id = {
$gt: ps.sinceId
};
} else if (ps.untilId) {
q._id = {
$lt: ps.untilId
};
}
const notes = await Note.find(q, {
limit: ps.limit,
sort: sort
});
return await packMany(notes, user);
});

View File

@@ -165,7 +165,10 @@ export const meta = {
choices: $.arr($.str)
.unique()
.range(2, 10)
.each(c => c.length > 0 && c.length < 50)
.each(c => c.length > 0 && c.length < 50),
multiple: $.optional.bool,
expiresAt: $.optional.nullable.num.int(),
expiredAfter: $.optional.nullable.num.int().min(1)
}).strict(),
desc: {
'ja-JP': 'アンケート'
@@ -214,6 +217,12 @@ export const meta = {
code: 'CONTENT_REQUIRED',
id: '6f57e42b-c348-439b-bc45-993995cc515a'
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
id: '04da457d-b083-4055-9082-955525eda5a5'
}
}
};
@@ -275,6 +284,13 @@ export default define(meta, async (ps, user, app) => {
text: choice.trim(),
votes: 0
}));
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now())
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
} else if (typeof ps.poll.expiredAfter === 'number') {
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
}
}
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
@@ -291,7 +307,11 @@ export default define(meta, async (ps, user, app) => {
const note = await create(user, {
createdAt: new Date(),
files: files,
poll: ps.poll,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple || false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
} : undefined,
text: ps.text,
reply,
renote,

View File

@@ -7,10 +7,13 @@ import watch from '../../../../../services/note/watch';
import { publishNoteStream } from '../../../../../services/stream';
import notify from '../../../../../services/create-notification';
import define from '../../../define';
import createNote from '../../../../../services/note/create';
import User from '../../../../../models/user';
import User, { IRemoteUser } from '../../../../../models/user';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { deliver } from '../../../../../queue';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderVote from '../../../../../remote/activitypub/renderer/vote';
import { deliverQuestionUpdate } from '../../../../../services/note/polls/update';
export const meta = {
desc: {
@@ -63,10 +66,18 @@ export const meta = {
code: 'ALREADY_VOTED',
id: '0963fc77-efac-419b-9424-b391608dc6d8'
},
alreadyExpired: {
message: 'The poll is already expired.',
code: 'ALREADY_EXPIRED',
id: '1022a357-b085-4054-9083-8f8de358337e'
},
}
};
export default define(meta, async (ps, user) => {
const createdAt = new Date();
// Get votee
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
@@ -77,23 +88,32 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.noPoll);
}
if (note.poll.expiresAt && note.poll.expiresAt < createdAt) {
throw new ApiError(meta.errors.alreadyExpired);
}
if (!note.poll.choices.some(x => x.id == ps.choice)) {
throw new ApiError(meta.errors.invalidChoice);
}
// if already voted
const exist = await Vote.findOne({
const exist = await Vote.find({
noteId: note._id,
userId: user._id
});
if (exist !== null) {
throw new ApiError(meta.errors.alreadyVoted);
if (exist.length) {
if (note.poll.multiple) {
if (exist.some(x => x.choice == ps.choice))
throw new ApiError(meta.errors.alreadyVoted);
} else {
throw new ApiError(meta.errors.alreadyVoted);
}
}
// Create vote
await Vote.insert({
createdAt: new Date(),
const vote = await Vote.insert({
createdAt,
noteId: note._id,
userId: user._id,
choice: ps.choice
@@ -146,18 +166,15 @@ export default define(meta, async (ps, user) => {
// リモート投票の場合リプライ送信
if (note._user.host != null) {
const pollOwner = await User.findOne({
const pollOwner: IRemoteUser = await User.findOne({
_id: note.userId
});
createNote(user, {
createdAt: new Date(),
text: ps.choice.toString(),
reply: note,
visibility: 'specified',
visibleUsers: [ pollOwner ],
});
deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox);
}
// リモートフォロワーにUpdate配信
deliverQuestionUpdate(note._id);
return;
});

View File

@@ -5,6 +5,7 @@ import hybridTimeline from './hybrid-timeline';
import globalTimeline from './global-timeline';
import notesStats from './notes-stats';
import serverStats from './server-stats';
import queueStats from './queue-stats';
import userList from './user-list';
import messaging from './messaging';
import messagingIndex from './messaging-index';
@@ -23,6 +24,7 @@ export default {
globalTimeline,
notesStats,
serverStats,
queueStats,
userList,
messaging,
messagingIndex,

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