Compare commits

...

140 Commits

Author SHA1 Message Date
60e0b19372 10.93.0 2019-03-12 17:24:42 +09:00
922eb937ff 🎨 2019-03-12 17:20:40 +09:00
87573284f1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-12 17:11:33 +09:00
a91c585f55 Add queue chart 2019-03-12 17:11:06 +09:00
953ea21d5e New Crowdin translations (#4479)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)
2019-03-12 10:41:05 +09:00
d89e0f07f8 Update issue templates 2019-03-12 10:38:30 +09:00
e7f81a42ce Resolve #4470 2019-03-12 10:35:17 +09:00
ac614148b8 Update README.md [AUTOGEN] (#4477) 2019-03-12 09:50:42 +09:00
5eb02b4901 Resolve #4458 (#4476) 2019-03-12 09:50:20 +09:00
65631525f6 Update README.md [AUTOGEN] (#4474) 2019-03-12 09:49:58 +09:00
969435cfe9 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-12 00:34:30 +09:00
c932f7a25b Resolve #1736 2019-03-12 00:34:19 +09:00
42d164dc57 log failed job (#4472) 2019-03-11 22:45:07 +09:00
a7e60f80bd Refactor: Extract downloadTextFile function 2019-03-11 20:23:29 +09:00
3dd5f313b7 Add icons 🎨 2019-03-11 20:12:44 +09:00
883962c393 Add icon 🎨 2019-03-11 20:07:27 +09:00
8a30ff1c76 🌎 A federated blogging platform 🚀 2019-03-11 20:03:02 +09:00
e47c354916 Refactor 2019-03-11 19:57:50 +09:00
496f42805d リストをインポートしたときにプロキシアカウントがフォローするように修正 2019-03-11 19:51:58 +09:00
c3d34bda37 Resolve #4259 2019-03-11 19:43:58 +09:00
bb6ede2b8f 10.92.4 2019-03-11 13:59:27 +09:00
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
e3e08843f1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-11 09:59:14 +09:00
ce0d4f77fa リストのエクスポートをできるように
#4259
2019-03-11 09:59:07 +09:00
94fdb4e974 Update README.md [AUTOGEN] (#4467) 2019-03-11 08:13:02 +09:00
4d425fc8a4 Use proxy instead of weserv for url-preview images (#4466) 2019-03-11 01:03:09 +09:00
c6cdfa2f5a Ignore 4xx references in AP (#4463)
* Ignore 4xx references

* remove unnecessary comment
2019-03-10 22:27:25 +09:00
0fff2e4f16 Remove debug code 2019-03-10 19:20:25 +09:00
80a2172715 Resolve #4462 2019-03-10 19:16:33 +09:00
5a0a297634 Improve redis config 2019-03-09 23:44:54 +09:00
948a133b7b Fix log 2019-03-09 18:15:44 +09:00
2ee826c958 10.92.3 2019-03-09 10:21:45 +09:00
539409faf8 Better logs 2019-03-09 10:18:59 +09:00
606e46e4d7 Make info 2019-03-09 10:10:24 +09:00
a179cfd69a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-09 10:09:12 +09:00
d8379253d4 Rename 2019-03-09 10:09:04 +09:00
c3344fbd68 To retry AP deliver queue (#4457) 2019-03-09 08:57:55 +09:00
4cebd6e84a Increase job attempts limit a little 2019-03-08 21:43:17 +09:00
90fbf9dbb0 Add missing semicolon 2019-03-08 21:38:23 +09:00
d365b9f634 Fix bug 2019-03-08 21:30:12 +09:00
a2f06acaa4 10.92.2 2019-03-08 21:00:35 +09:00
8c90cbcbfb Disable lifo 2019-03-08 20:58:01 +09:00
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
5dde1f4602 Update README.md 2019-03-08 20:52:09 +09:00
9dc0909eeb Add index 2019-03-08 20:36:42 +09:00
0ed2592e41 Resolve #4453 2019-03-08 20:07:29 +09:00
76cff98220 Fix bug 2019-03-08 19:45:01 +09:00
60604b6f51 🎨 2019-03-08 15:34:34 +09:00
f410b7aecb 🎨 2019-03-08 15:27:06 +09:00
1a61f2cee9 自動更新 2019-03-08 13:10:38 +09:00
78a8293520 ジョブの数を一覧できるように 2019-03-08 13:03:38 +09:00
03cfb4fc8d 🎨 2019-03-08 13:03:17 +09:00
144345a359 Enable lifo job 2019-03-08 05:39:59 +09:00
fd2c01515e Increase job concurrency 2019-03-08 05:23:13 +09:00
219570e08b Update job handlers 2019-03-08 05:22:14 +09:00
69df556ff5 Resolve #4448 2019-03-08 04:55:55 +09:00
5f4a52574f Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 23:43:56 +09:00
5a1f6c5839 10.92.1 2019-03-07 23:43:42 +09:00
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
8cc236daf8 Clean jobs 2019-03-07 23:36:08 +09:00
d283ec69f7 Refactor 2019-03-07 23:27:38 +09:00
d1aea7596c Fix import 2019-03-07 23:14:01 +09:00
c934987b14 Resolve #4444 2019-03-07 23:07:21 +09:00
00c9f4a2e5 Update issue templates 2019-03-07 21:41:39 +09:00
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
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
a485061e22 Update README.md [AUTOGEN] (#4441) 2019-03-07 20:30:41 +09:00
1f63f50343 Create robots.txt (#4439)
* Create robots.txt

* Update index.ts
2019-03-07 20:11:04 +09:00
cd3170dabd Update PULL_REQUEST_TEMPLATE.md 2019-03-07 20:06:36 +09:00
841cedc5f8 Update issue templates 2019-03-07 19:56:02 +09:00
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
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
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
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
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
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
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
1b9cf76008 Update issue templates 2019-03-07 18:16:49 +09:00
d035a43ed6 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 16:50:41 +09:00
95ee9a6e09 Update issue templates 2019-03-07 16:50:05 +09:00
02a63cdcb3 Update index.js 2019-03-07 16:43:16 +09:00
f02125dd47 Resolve #4437 2019-03-07 13:03:46 +09:00
c11e813146 Fix 2019-03-07 09:35:23 +09:00
a365849048 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-07 09:31:07 +09:00
a493c9f769 🎨 2019-03-07 09:30:44 +09:00
a13f522b2a Update README.md [AUTOGEN] (#4436) 2019-03-07 09:18:57 +09:00
1ed70b2e2c 10.92.0 2019-03-06 23:37:53 +09:00
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
c226fc8d63 Make building locales better (#4427) 2019-03-06 23:28:50 +09:00
bbf4e1c413 Add fallback for broken db 2019-03-06 23:23:50 +09:00
a24a20a83d Clean up 2019-03-06 23:19:15 +09:00
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
f74a32ed9b [Client] Fix bug 2019-03-06 20:17:59 +09:00
e08e72dd10 Use meta url instead of constant url 2019-03-06 12:31:58 +09:00
ce02e1e528 Bye on-build-webpack (#4404)
* Update webpack.config.ts

* Update package.json
2019-03-06 09:26:22 +09:00
0b27d8a717 Fix #4421 2019-03-06 09:24:16 +09:00
2782e7d26f [MFM] Improve hashtag parsing 2019-03-05 22:18:29 +09:00
2c83a05e80 [Client] Better transition 2019-03-05 20:45:19 +09:00
467f68502a Refactor 2019-03-05 20:32:39 +09:00
d95b0dee6b Fix #4416 2019-03-05 08:27:50 +09:00
a1f3323fa5 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-05 08:24:26 +09:00
494796a7f0 Fix #4415 2019-03-05 08:24:19 +09:00
94f2c20d35 Fix #4413 (#4417) 2019-03-05 07:56:23 +09:00
c1deb9438d Update README.md [AUTOGEN] (#4411) 2019-03-04 19:27:53 +09:00
ea86527c66 Add indexes 2019-03-04 19:26:05 +09:00
d1a18fe266 Resolve #3220 2019-03-04 14:32:36 +09:00
737064da82 Better log 2019-03-04 14:02:42 +09:00
606cc85ff5 10.91.2 2019-03-04 09:25:28 +09:00
dcfc8f1b30 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-04 09:23:49 +09:00
ebe4b84f14 Fix bug 2019-03-04 09:23:45 +09:00
699d4897db New Crowdin translations (#4403)
* 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 (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 (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 (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 (Polish)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* 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)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)
2019-03-04 09:17:46 +09:00
fcdfd8d323 Use vue-json-pretty 2019-03-04 09:16:24 +09:00
db8625c31a Revert "Improve log view" (#4406)
This reverts commit e8ef8f0004.
2019-03-04 08:55:35 +09:00
b65f265c55 Update logs 2019-03-03 10:57:17 +09:00
c55237d09c 10.91.1 2019-03-03 08:49:53 +09:00
ed698b7b82 New Crowdin translations (#4398)
* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)
2019-03-03 08:48:17 +09:00
d4ff19f013 Fix SVG detection (#4401)
* Fix SVG detection

* remove unnecessary import
2019-03-03 08:48:02 +09:00
972fb8eb40 Hide some components 2019-03-03 08:46:42 +09:00
4de75448b6 テーマの切り替えをなめらかに 2019-03-03 08:43:51 +09:00
e8ef8f0004 Improve log view 2019-03-03 08:35:30 +09:00
a319b30382 Increase log limit 2019-03-03 08:28:35 +09:00
8278616eeb Fix log order 2019-03-03 08:28:12 +09:00
771f011506 Better log 2019-03-03 08:27:30 +09:00
826865869a Improve usability 2019-03-03 08:23:06 +09:00
3c77ae7b62 Improve log api 2019-03-03 08:13:49 +09:00
60c30ece10 Better logs 2019-03-03 08:00:39 +09:00
76a0d0fee9 Use dot 2019-03-03 07:48:25 +09:00
d50624f0a0 Better logs 2019-03-03 07:47:58 +09:00
135 changed files with 4651 additions and 12099 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,68 @@
ChangeLog ChangeLog
========= =========
If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`)
10.93.0
----------
* フォローリストをインポートできるように
* embedプレイヤーを閉じれるように
* リストをインポートしたときにプロキシアカウントがフォローするように修正
* Web Share Targetの動作を修正
* おすすめアンケートのチョイスを修正
* デザインの調整
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脆弱性があったので他のパッケージに差し替え
* 初期アクセスでテーマが正しく設定されない問題を修正
10.91.1
----------
* ログビューを強化
* テーマの切り替えをなめらかに
* SVGの判定を修正
10.91.0 10.91.0
---------- ----------
* ログを管理画面で見れるように * ログを管理画面で見れるように

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/) [![Misskey](/assets/title.png)](https://misskey.xyz/)
================================================================ ================================================================
@ -103,7 +103,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<table><tr> <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://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://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://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/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> <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://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/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/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> </tr><tr>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td> <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> <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/user?u=17463605">Sampot</a></td>
<td><a href="https://www.patreon.com/takimura">takimura</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/damillora">Damillora</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
</tr></table> </tr></table>
<table><tr> <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/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" 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/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/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> <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> </tr><tr>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td> <td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td> <td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td> <td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/mastodon">Gargron</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/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Fri, 01 Mar 2019 23:59:07 UTC **Last updated:** Tue, 12 Mar 2019 00:50:06 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright

4
assets/robots.txt Normal file
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

1193
locales/cs-CZ.yml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
--- ---
meta: meta:
lang: "English" lang: "English"
divider: ""
common: common:
misskey: "A ⭐ of the fediverse" misskey: "A ⭐ of the fediverse"
about-title: "A ⭐ of the fediverse." about-title: "A ⭐ of the fediverse."
@ -25,8 +24,8 @@ common:
application-authorization: "Application authorizations" application-authorization: "Application authorizations"
close: "Close" close: "Close"
do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised." do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
load-more: "Load more" load-more: "Read more"
enter-password: "Please enter the Password" enter-password: "Enter your password"
2fa: "Two-factor authentication" 2fa: "Two-factor authentication"
customize-home: "Customize home layout" customize-home: "Customize home layout"
featured-notes: "Featured notes" featured-notes: "Featured notes"
@ -115,7 +114,7 @@ common:
a: "What are you doing?" a: "What are you doing?"
b: "What's happening?" b: "What's happening?"
c: "Whats on your mind?" c: "Whats on your mind?"
d: "Would you post any words?" d: "What do you want to say?"
e: "Write here" e: "Write here"
f: "Waiting for your writing." f: "Waiting for your writing."
settings: "Settings" settings: "Settings"
@ -224,8 +223,8 @@ common:
search: "Search" search: "Search"
delete: "Delete" delete: "Delete"
loading: "Loading" loading: "Loading"
ok: "It's OK" ok: "Confirm"
cancel: "Quit" cancel: "Exit"
update-available-title: "Update available" 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." 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." my-token-regenerated: "Your token has been regenerated, so you will be signed out."
@ -286,7 +285,7 @@ auth/views/form.vue:
account-read: "View account information." account-read: "View account information."
account-write: "Modify account information." account-write: "Modify account information."
note-write: "Post." note-write: "Post."
like-write: "React to posts." like-write: "Express yourself about this post."
following-write: "Follow and unfollow." following-write: "Follow and unfollow."
drive-read: "Read your drive." drive-read: "Read your drive."
drive-write: "Upload/delete files in your drive." drive-write: "Upload/delete files in your drive."
@ -305,7 +304,7 @@ auth/views/index.vue:
error: "Session does not exist." error: "Session does not exist."
sign-in: "Please sign in." sign-in: "Please sign in."
common/views/pages/explore.vue: common/views/pages/explore.vue:
verified-users: "Verified accounts" verified-users: "Official accounts"
popular-users: "Popular users" popular-users: "Popular users"
recently-updated-users: "Recently active users" recently-updated-users: "Recently active users"
recently-registered-users: "Users who joined recently" recently-registered-users: "Users who joined recently"
@ -315,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "Currently, {users} users are registered here" users-info: "Currently, {users} users are registered here"
common/views/components/url-preview.vue: common/views/components/url-preview.vue:
enable-player: "Enable playback" enable-player: "Enable playback"
disable-player: "Close the player"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "There are no users." no-users: "There are no users."
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
@ -490,16 +490,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "Vote for '{}'" vote-to: "Vote for '{}'"
vote-count: "{} votes" vote-count: "{} votes"
total-users: "{} users voted" total-votes: "{} votes in total"
vote: "Vote" vote: "Vote"
show-result: "Show results" show-result: "Show results"
voted: "Voted" 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: common/views/components/poll-editor.vue:
no-only-one-choice: "At least two choices are required" no-only-one-choice: "At least two choices are required"
choice-n: "Choice {}" choice-n: "Choice {}"
remove: "Delete the choice" remove: "Delete the choice"
add: "+ Add a choice" add: "+ Add a choice"
destroy: "Discard the poll" 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: common/views/components/reaction-picker.vue:
choose-reaction: "Send a reaction" choose-reaction: "Send a reaction"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
@ -521,7 +540,7 @@ common/views/components/signin.vue:
signin-with-twitter: "Log in with Twitter" signin-with-twitter: "Log in with Twitter"
signin-with-github: "Sign in with GitHub" signin-with-github: "Sign in with GitHub"
signin-with-discord: "Sign in with Discord" 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: common/views/components/signup.vue:
invitation-code: "Invitation code" invitation-code: "Invitation code"
invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>." invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>."
@ -629,12 +648,16 @@ common/views/components/profile-editor.vue:
email-verified: "Your email has been verified." email-verified: "Your email has been verified."
email-not-verified: "Email address is not confirmed. Please check your inbox." email-not-verified: "Email address is not confirmed. Please check your inbox."
export: "Export" export: "Export"
import: "Import"
export-and-import: "Export and Import"
export-targets: export-targets:
all-notes: "All posted Notes" all-notes: "All posted Notes"
following-list: "List of followers" following-list: "List of followers"
mute-list: "List of muted accounts" mute-list: "List of muted accounts"
blocking-list: "List of blocked 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." export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
import-requested: "You have initiated an import. This may take quite some time."
enter-password: "Please enter your password" enter-password: "Please enter your password"
danger-zone: "Cautious options" danger-zone: "Cautious options"
delete-account: "Remove the account" delete-account: "Remove the account"
@ -972,7 +995,7 @@ desktop/views/components/timeline.vue:
messages: "Messages" messages: "Messages"
list: "Lists" list: "Lists"
hashtag: "Hashtag" hashtag: "Hashtag"
add-tag-timeline: "Add hashtag tl" add-tag-timeline: "Add hashtag cloud"
add-list: "Add list" add-list: "Add list"
list-name: "List name" list-name: "List name"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
@ -1023,6 +1046,7 @@ admin/views/index.vue:
hashtags: "Hashtags" hashtags: "Hashtags"
abuse: "Abuse" abuse: "Abuse"
queue: "Job Queue" queue: "Job Queue"
logs: "Logs"
back-to-misskey: "Back to Misskey" back-to-misskey: "Back to Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Dashboard" dashboard: "Dashboard"
@ -1196,7 +1220,7 @@ admin/views/users.vue:
updatedAtAsc: "Last Updated (Ascending)" updatedAtAsc: "Last Updated (Ascending)"
updatedAtDesc: "Last Updated (Descending)" updatedAtDesc: "Last Updated (Descending)"
state: state:
title: "Status" title: "Sort"
all: "All" all: "All"
admin: "Administrator" admin: "Administrator"
moderator: "Moderator" moderator: "Moderator"
@ -1257,7 +1281,7 @@ admin/views/federation.vue:
users: "Users" users: "Users"
following: "Following" following: "Following"
followers: "Followers" followers: "Followers"
status: "Status" status: "Statuses"
latest-request-sent-at: "Time of last request sent" latest-request-sent-at: "Time of last request sent"
latest-request-received-at: "Last request received at" latest-request-received-at: "Last request received at"
remove-all-following: "Withold all followers" remove-all-following: "Withold all followers"
@ -1273,19 +1297,19 @@ admin/views/federation.vue:
caughtAtDesc: "Date of discovery (Descending)" caughtAtDesc: "Date of discovery (Descending)"
lastCommunicatedAtAsc: "The date and time of the older interactions" lastCommunicatedAtAsc: "The date and time of the older interactions"
lastCommunicatedAtDesc: "The date and time of the newer interactions" lastCommunicatedAtDesc: "The date and time of the newer interactions"
notesAsc: "Order by least Notes posted" notesAsc: "Least Notes posted"
notesDesc: "Order by most Notes posted" notesDesc: "Most Notes posted"
usersAsc: "Less followers" usersAsc: "Less followers"
usersDesc: "More followers" usersDesc: "More followers"
followingAsc: "Least followed" followingAsc: "Least followed"
followingDesc: "Has more followers" followingDesc: "Most followed"
followersAsc: "Sort by having less followers" followersAsc: "Having less followers"
followersDesc: "Sort by the larger number of followers" followersDesc: "The largest number of followers"
driveUsageAsc: "Least storage used" driveUsageAsc: "Least storage used"
driveUsageDesc: "Most storage used" driveUsageDesc: "Most storage used"
driveFilesAsc: "By the smallest number of files stored on Drive" driveFilesAsc: "Least files stored on Drive"
driveFilesDesc: "By the largest number of files stored on Drive" driveFilesDesc: "The largest number of files stored on Drive"
state: "Status" state: "Sort"
states: states:
all: "All" all: "All"
blocked: "Blocked" blocked: "Blocked"
@ -1328,8 +1352,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "Search feature is turned off in the settings for this instance." not-available: "Search feature is turned off in the settings for this instance."
not-found: "No posts were found for '{q}'" not-found: "No posts were found for '{q}'"
desktop/views/pages/share.vue:
share-with: "Share on {name}"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "No posts contains \"{q}\" found." no-posts-found: "No posts contains \"{q}\" found."
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
@ -1353,7 +1375,7 @@ desktop/views/pages/user/user.header.vue:
following: "Following" following: "Following"
followers: "Followers" followers: "Followers"
is-bot: "This account is a Bot" 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" years-old: "{age} years old"
year: "/" year: "/"
month: "/" month: "/"
@ -1365,7 +1387,7 @@ desktop/views/pages/user/user.timeline.vue:
with-media: "Media" with-media: "Media"
my-posts: "My posts" my-posts: "My posts"
desktop/views/widgets/messaging.vue: desktop/views/widgets/messaging.vue:
title: "Message" title: "Messaging"
desktop/views/widgets/notifications.vue: desktop/views/widgets/notifications.vue:
title: "Notifications" title: "Notifications"
desktop/views/widgets/polls.vue: desktop/views/widgets/polls.vue:
@ -1558,8 +1580,8 @@ deck:
stack-left: "Stack to the left" stack-left: "Stack to the left"
pop-right: "Dock on the right" pop-right: "Dock on the right"
disabled-timeline: disabled-timeline:
title: "Timeline has been disabled" title: "The timeline has been disabled"
description: "Timeline has been disabled by the administrator." description: "This timeline has been disabled by the server's administrator."
deck/deck.tl-column.vue: deck/deck.tl-column.vue:
is-media-only: "Only media posts" is-media-only: "Only media posts"
edit: "Options" edit: "Options"

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
--- ---
meta: meta:
lang: "Français" lang: "Français"
divider: ""
common: common:
misskey: "Une ⭐ du fédiverse" misskey: "Une ⭐ du fédiverse"
about-title: "Une ⭐ du fédiverse." about-title: "Une ⭐ du fédiverse."
@ -30,15 +29,9 @@ common:
2fa: "Authentification à deux facteurs" 2fa: "Authentification à deux facteurs"
customize-home: "Personnaliser la disposition de votre accueil" customize-home: "Personnaliser la disposition de votre accueil"
featured-notes: "Les notes mises en avant" featured-notes: "Les notes mises en avant"
dark-mode: "ダークモード"
signin: "ログイン"
signup: "新規登録"
signout: "ログアウト"
reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
got-it: "Jai compris !" got-it: "Jai compris !"
customization-tips: customization-tips:
title: "Conseils de personnalisation" title: "Conseils de personnalisation"
paragraph: "<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p><p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p><p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p><p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>"
gotit: "Compris !" gotit: "Compris !"
notification: notification:
file-uploaded: "Le fichier a été téléversé !" file-uploaded: "Le fichier a été téléversé !"
@ -69,7 +62,7 @@ common:
explore: "Découvrir" explore: "Découvrir"
following: "Suit" following: "Suit"
followers: "Abonné·e·s" followers: "Abonné·e·s"
favorites: "お気に入り" favorites: "Mettre cette note en favoris"
empty-timeline-info: empty-timeline-info:
follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil." follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil."
explore: "Trouver des utilisateurs" explore: "Trouver des utilisateurs"
@ -118,114 +111,17 @@ common:
d: "Désirez-vous publier quelques mots ?" d: "Désirez-vous publier quelques mots ?"
e: "Écrivez ici" e: "Écrivez ici"
f: "En attente de vos écrits" f: "En attente de vos écrits"
settings: "設定"
_settings: _settings:
profile: "プロフィール" profile: "Votre profil"
notification: "通知" notification: "Notifications"
apps: "アプリ" tags: "Hashtags"
tags: "ハッシュタグ" blocking: "En cours blocage"
mute-and-block: "ミュート/ブロック" password: "Mot de passe"
blocking: "ブロック" other: "Avancé"
security: "セキュリティ" timeline: "Fil dactualité"
signin: "ログイン履歴"
password: "パスワード"
other: "その他"
appearance: "デザイン"
behavior: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
note-visibility: "投稿の公開範囲"
default-note-visibility: "デフォルトの公開範囲"
remember-note-visibility: "投稿の公開範囲を記憶する"
web-search-engine: "ウェブ検索エンジン"
web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
keep-cw: "CW保持"
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
use-avatar-reversi-stones: "リバーシの石にアバターを使う"
disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
disable-showing-animated-images: "アニメーション画像を再生しない"
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない"
show-via: "viaを表示する"
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
use-os-default-emojis: "OS標準の絵文字を使用"
line-width: "線の太さ"
line-width-thin: "細い"
line-width-normal: "普通"
line-width-thick: "太い"
font-size: "文字の大きさ"
font-size-x-small: "小さい"
font-size-small: "少し小さい"
font-size-medium: "普通"
font-size-large: "少し大きい"
font-size-x-large: "大きい"
deck-column-align: "デッキのカラムの配置"
deck-column-align-center: "中央"
deck-column-align-left: "左"
deck-column-align-flexible: "フレキシブル"
deck-column-width: "デッキのカラムの幅"
deck-column-width-narrow: "狭"
deck-column-width-narrower: "やや狭"
deck-column-width-normal: "普通"
deck-column-width-wider: "やや広"
deck-column-width-wide: "広"
use-shadow: "UIに影を使用"
rounded-corners: "UIの角を丸める"
circle-icons: "円形のアイコンを使用"
contrasted-acct: "ユーザー名にコントラストを付ける"
wallpaper: "壁紙"
choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
show-clock-on-header: "右上に時計を表示する"
show-reply-target: "リプライ先を表示する"
timeline: "タイムライン"
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する"
remain-deleted-note: "削除された投稿を表示し続ける"
sound: "サウンド"
enable-sounds: "サウンドを有効にする"
enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
volume: "ボリューム"
test: "テスト"
update: "Misskey Update"
version: "バージョン:"
latest-version: "最新のバージョン:"
update-checking: "アップデートを確認中"
do-update: "アップデートを確認"
update-settings: "詳細設定"
no-updates: "利用可能な更新はありません"
no-updates-desc: "お使いのMisskeyは最新です。"
update-available: "新しいバージョンが利用可能です"
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
advanced-settings: "高度な設定"
debug-mode: "デバッグモードを有効にする"
debug-mode-desc: "この設定はブラウザに記憶されます。"
navbar-position: "ナビゲーションバーの位置"
navbar-position-top: "上"
navbar-position-left: "左"
navbar-position-right: "右"
i-am-under-limited-internet: "私は通信を制限されている"
post-style: "投稿の表示スタイル"
post-style-standard: "標準"
post-style-smart: "スマート"
notification-position: "通知の表示"
notification-position-bottom: "下"
notification-position-top: "上"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
load-raw-images: "添付された画像を高画質で表示する"
load-remote-media: "リモートサーバーのメディアを表示する"
search: "Recherche" search: "Recherche"
delete: "Supprimer" delete: "Supprimer"
loading: "Chargement en cours…" loading: "Chargement en cours…"
ok: "おk"
cancel: "やめる"
update-available-title: "Mise à jour disponible" update-available-title: "Mise à jour disponible"
update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour." update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour."
my-token-regenerated: "Votre jeton vient dêtre généré, vous allez maintenant être déconnecté." my-token-regenerated: "Votre jeton vient dêtre généré, vous allez maintenant être déconnecté."
@ -313,8 +209,6 @@ common/views/pages/explore.vue:
federated: "Du Fédiverse" federated: "Du Fédiverse"
explore: "Explorer {host}" explore: "Explorer {host}"
users-info: "Actuellement, {users} utilisateurs se sont inscrit ici" users-info: "Actuellement, {users} utilisateurs se sont inscrit ici"
common/views/components/url-preview.vue:
enable-player: "プレイヤーを開く"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "Il n'y a aucun utilisateur" no-users: "Il n'y a aucun utilisateur"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
@ -349,7 +243,6 @@ common/views/components/games/reversi/reversi.room.vue:
black-or-white: "Noirs/Blancs" black-or-white: "Noirs/Blancs"
black-is: "{} Noirs" black-is: "{} Noirs"
rules: "Règles" rules: "Règles"
is-llotheo: "石の少ない方が勝ち(ロセオ)"
looped-map: "Carte en boucle" looped-map: "Carte en boucle"
can-put-everywhere: "Peut poser partout" can-put-everywhere: "Peut poser partout"
settings-of-the-bot: "Configuration du bot" settings-of-the-bot: "Configuration du bot"
@ -490,7 +383,6 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "Voter pour '{}'" vote-to: "Voter pour '{}'"
vote-count: "{} votes" vote-count: "{} votes"
total-users: "{} utilisateur·rice·s ont voté"
vote: "Vote" vote: "Vote"
show-result: "Montrer les résultats" show-result: "Montrer les résultats"
voted: "Voté" voted: "Voté"
@ -500,6 +392,7 @@ common/views/components/poll-editor.vue:
remove: "Supprimer ce choix" remove: "Supprimer ce choix"
add: "+ Ajouter un choix" add: "+ Ajouter un choix"
destroy: "Annuler ce sondage" destroy: "Annuler ce sondage"
day: "D"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "Choisissez votre réaction" choose-reaction: "Choisissez votre réaction"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
@ -629,11 +522,13 @@ common/views/components/profile-editor.vue:
email-verified: "Ladresse du courrier électronique a été vérifiée." email-verified: "Ladresse du courrier électronique a été vérifiée."
email-not-verified: "Adresse de courriel nest pas confirmée. Veuillez vérifier votre boite de réception." email-not-verified: "Adresse de courriel nest pas confirmée. Veuillez vérifier votre boite de réception."
export: "Exporter" export: "Exporter"
import: "Importer"
export-targets: export-targets:
all-notes: "Toutes les notes publiées" all-notes: "Toutes les notes publiées"
following-list: "Liste des abonnements" following-list: "Liste des abonnements"
mute-list: "Liste des comptes mis en sourdine" mute-list: "Liste des comptes mis en sourdine"
blocking-list: "Liste des comptes bloqués" 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." 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" enter-password: "Veuillez saisir votre mot de passe"
danger-zone: "Zone de danger" danger-zone: "Zone de danger"
@ -695,7 +590,6 @@ common/views/widgets/tips.vue:
tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur." tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur."
tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé" tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé"
tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots." tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots."
tips-line23: "藍かわいいよ藍"
tips-line24: "Misskey est fonctionnel depuis 2014" tips-line24: "Misskey est fonctionnel depuis 2014"
tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible" tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible"
common/views/pages/not-found.vue: common/views/pages/not-found.vue:
@ -1055,7 +949,6 @@ admin/views/instance.vue:
maintainer-email: "Contact administratif" maintainer-email: "Contact administratif"
drive-config: "Paramètres du lecteur" drive-config: "Paramètres du lecteur"
cache-remote-files: "Mettre en cache des fichiers distants" cache-remote-files: "Mettre en cache des fichiers distants"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。"
local-drive-capacity-mb: "Volume du lecteur par utilisateur" local-drive-capacity-mb: "Volume du lecteur par utilisateur"
remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant" remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant"
mb: "en mégaoctets" mb: "en mégaoctets"
@ -1080,7 +973,6 @@ admin/views/instance.vue:
discord-integration-client-id: "ID client" discord-integration-client-id: "ID client"
discord-integration-client-secret: "Secret client" discord-integration-client-secret: "Secret client"
proxy-account-config: "Compte proxy" proxy-account-config: "Compte proxy"
proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
proxy-account-username: "Nom dutilisateur du compte proxy" proxy-account-username: "Nom dutilisateur du compte proxy"
proxy-account-username-desc: "Spécifiez le nom dutilisateur du compte utilisé comme proxy." proxy-account-username-desc: "Spécifiez le nom dutilisateur du compte utilisé comme proxy."
proxy-account-warn: "Avant dentamer cette action, vous devez au préalable avoir créé un compte avec ce nom dutilisateur." proxy-account-warn: "Avant dentamer cette action, vous devez au préalable avoir créé un compte avec ce nom dutilisateur."
@ -1260,38 +1152,29 @@ admin/views/federation.vue:
status: "Statuts" status: "Statuts"
latest-request-sent-at: "Dernière requête envoyée" latest-request-sent-at: "Dernière requête envoyée"
latest-request-received-at: "Dernière requête reçue" latest-request-received-at: "Dernière requête reçue"
remove-all-following: "フォローを全解除"
remove-all-following-info: "Se désabonner de tous les comptes de {host}. Exécutez cette commande si l'instance n'existe plus." remove-all-following-info: "Se désabonner de tous les comptes de {host}. Exécutez cette commande si l'instance n'existe plus."
block: "Bloquer" block: "Bloquer"
marked-as-closed: "Marquées comme fermées" marked-as-closed: "Marquées comme fermées"
lookup: "Recherche" lookup: "Recherche"
instances: "Instances" instances: "Instances"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "Trier par" sort: "Trier par"
sorts: sorts:
caughtAtAsc: "Date dinscription (Ascendant)" caughtAtAsc: "Date dinscription (Ascendant)"
caughtAtDesc: "Date dinscription (Descendant)" caughtAtDesc: "Date dinscription (Descendant)"
lastCommunicatedAtAsc: "La date et l'heure des interactions plus anciennes" lastCommunicatedAtAsc: "La date et l'heure des interactions plus anciennes"
lastCommunicatedAtDesc: "La date et l'heure des nouvelles interactions" lastCommunicatedAtDesc: "La date et l'heure des nouvelles interactions"
notesAsc: "投稿が少ない順"
notesDesc: "Description des notes" notesDesc: "Description des notes"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "Les moins suivies" followingAsc: "Les moins suivies"
followingDesc: "Ayant le plus d'abonné·e·s" followingDesc: "Ayant le plus d'abonné·e·s"
followersAsc: "Ayant le moins d'abonné·e·s" followersAsc: "Ayant le moins d'abonné·e·s"
followersDesc: "Ayant le plus d'abonné·e·s" followersDesc: "Ayant le plus d'abonné·e·s"
driveUsageAsc: "Moins d'espace de stockage utilisé" driveUsageAsc: "Moins d'espace de stockage utilisé"
driveUsageDesc: "ドライブ使用量が多い順"
driveFilesAsc: "ドライブのファイル数が少ない順"
driveFilesDesc: "ドライブのファイル数が多い順"
state: "État" state: "État"
states: states:
all: "Tout" all: "Tout"
blocked: "Bloquées" blocked: "Bloquées"
not-responding: "Sans réponse" not-responding: "Sans réponse"
marked-as-closed: "Marquée comme fermée" marked-as-closed: "Marquée comme fermée"
result-is-truncated: "上位{n}件を表示しています。"
charts: "Graphs" charts: "Graphs"
chart-srcs: chart-srcs:
requests: "Requêtes" requests: "Requêtes"
@ -1300,10 +1183,8 @@ admin/views/federation.vue:
notes: "Augmentation/diminution du nombre des notes" notes: "Augmentation/diminution du nombre des notes"
notes-total: "Nombre total des notes" notes-total: "Nombre total des notes"
ff: "Augmentation des abonné·e·s" ff: "Augmentation des abonné·e·s"
ff-total: "フォロー/フォロワーの積算"
drive-usage: "Augmentation et diminution de la capacité stockage" drive-usage: "Augmentation et diminution de la capacité stockage"
drive-usage-total: "Utilisation totale du stockage" drive-usage-total: "Utilisation totale du stockage"
drive-files: "ドライブファイル数の増減"
drive-files-total: "Nombre total des fichiers sur le Drive" drive-files-total: "Nombre total des fichiers sur le Drive"
chart-spans: chart-spans:
hour: "Par heure" hour: "Par heure"
@ -1328,8 +1209,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "La fonction de recherche est désactivée dans les paramètres de linstance." not-available: "La fonction de recherche est désactivée dans les paramètres de linstance."
not-found: "Aucune publication trouvée pour « {q} »." not-found: "Aucune publication trouvée pour « {q} »."
desktop/views/pages/share.vue:
share-with: "Partager avec {name}"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "Aucune publication contenant « {q} » na été trouvée." no-posts-found: "Aucune publication contenant « {q} » na été trouvée."
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
@ -1396,7 +1275,6 @@ mobile/views/components/drive.vue:
prompt: "Que veux-tu faire ? (Entrez un nombre): <1 → Télécharger le fichier | 2 → Télécharger le fichier avec l'URL | 3 → Créer le dossier | 4 → Modifier le nom du dossier | 5 → Déplacer ce dossier | 6 → Supprimer ce dossier >" prompt: "Que veux-tu faire ? (Entrez un nombre): <1 → Télécharger le fichier | 2 → Télécharger le fichier avec l'URL | 3 → Créer le dossier | 4 → Modifier le nom du dossier | 5 → Déplacer ce dossier | 6 → Supprimer ce dossier >"
deletion-alert: "Désolé ! La suppression dun dossier nest pas encore implémentée." deletion-alert: "Désolé ! La suppression dun dossier nest pas encore implémentée."
folder-name: "Nom du dossier" folder-name: "Nom du dossier"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "URL du fichier que vous souhaitez téléverser" url-prompt: "URL du fichier que vous souhaitez téléverser"
uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever." uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever."
mobile/views/components/drive-file-chooser.vue: mobile/views/components/drive-file-chooser.vue:
@ -1491,10 +1369,9 @@ mobile/views/pages/home.vue:
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "Aucune publication ayant pour hashtag « {q} » na été trouvée." no-posts-found: "Aucune publication ayant pour hashtag « {q} » na été trouvée."
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "ダッシュボード" dashboard: "Tableau de bord"
widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。" add-widget: "Ajouter"
add-widget: "追加" customization-tips: "Conseils de personnalisation"
customization-tips: "カスタマイズのヒント"
mobile/views/pages/widgets/activity.vue: mobile/views/pages/widgets/activity.vue:
activity: "Activité" activity: "Activité"
mobile/views/pages/share.vue: mobile/views/pages/share.vue:
@ -1547,7 +1424,7 @@ deck:
direct: "Messages directs" direct: "Messages directs"
notifications: "Notifications" notifications: "Notifications"
list: "Listes" list: "Listes"
select-list: "リストを選択してください" select-list: "Sélectionnez une liste"
swap-left: "Déplacer à gauche" swap-left: "Déplacer à gauche"
swap-right: "Déplacer à droite" swap-right: "Déplacer à droite"
swap-up: "Déplacer vers le haut" swap-up: "Déplacer vers le haut"

View File

@ -5,22 +5,46 @@
const fs = require('fs'); const fs = require('fs');
const yaml = require('js-yaml'); 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 languages = [
const locales = langs 'de-DE',
.map(lang => [lang, loadLocale(lang)]) 'en-US',
.map(([lang, locale], _, locales) => { 'es-ES',
switch (lang) { 'fr-FR',
case 'ja-JP': return [lang, locale]; 'ja-JP',
case 'en-US': return [lang, { ...locales['ja-JP'], ...locale }]; 'ja-KS',
default: return [lang, { 'ko-KR',
...(lang.startsWith('ja-') ? {} : locales['en-US']), 'nl-NL',
...locales['ja-JP'], 'pl-PL',
...locale '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
);
} }
}) })(), a), {});
.map(([lang, locale]) => ({ [lang]: loadLocale(lang) }));
module.exports = locales.reduce((a, b) => ({ ...a, ...b }));

File diff suppressed because it is too large Load Diff

View File

@ -334,6 +334,7 @@ common/views/pages/explore.vue:
common/views/components/url-preview.vue: common/views/components/url-preview.vue:
enable-player: "プレイヤーを開く" enable-player: "プレイヤーを開く"
disable-player: "プレイヤーを閉じる"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "ユーザーがいません" no-users: "ユーザーがいません"
@ -527,10 +528,15 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "「{}」に投票する" vote-to: "「{}」に投票する"
vote-count: "{}票" vote-count: "{}票"
total-users: "{}人が投票" total-votes: "{}票"
vote: "投票する" vote: "投票する"
show-result: "結果を見る" show-result: "結果を見る"
voted: "投票済み" voted: "投票済み"
closed: "終了済み"
remaining-days: "終了まであと{d}日{h}時間"
remaining-hours: "終了まであと{h}時間{m}分"
remaining-minutes: "終了まであと{m}分{s}秒"
remaining-seconds: "終了まであと{s}秒"
common/views/components/poll-editor.vue: common/views/components/poll-editor.vue:
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です" no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
@ -538,6 +544,20 @@ common/views/components/poll-editor.vue:
remove: "この選択肢を削除" remove: "この選択肢を削除"
add: "+選択肢を追加" add: "+選択肢を追加"
destroy: "アンケートを破棄" destroy: "アンケートを破棄"
multiple: "複数回答可"
expiration: "期限"
infinite: "無期限"
at: "日時指定"
after: "経過指定"
no-more: "これ以上追加できません"
deadline-date: "期日"
deadline-time: "時間"
interval: "期間"
unit: "単位"
second: "秒"
minute: "分"
hour: "時間"
day: "日"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択" choose-reaction: "リアクションを選択"
@ -682,12 +702,16 @@ common/views/components/profile-editor.vue:
email-verified: "メールアドレスが確認されました" email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
export: "エクスポート" export: "エクスポート"
import: "インポート"
export-and-import: "エクスポートとインポート"
export-targets: export-targets:
all-notes: "すべての投稿データ" all-notes: "すべての投稿データ"
following-list: "フォロー" following-list: "フォロー"
mute-list: "ミュート" mute-list: "ミュート"
blocking-list: "ブロック" blocking-list: "ブロック"
user-lists: "リスト"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。" export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
import-requested: "インポートをリクエストしました。これには時間がかかる場合があります。"
enter-password: "パスワードを入力してください" enter-password: "パスワードを入力してください"
danger-zone: "危険な設定" danger-zone: "危険な設定"
delete-account: "アカウントを削除" delete-account: "アカウントを削除"
@ -1156,7 +1180,7 @@ admin/views/dashboard.vue:
federated: "連合" federated: "連合"
admin/views/queue.vue: admin/views/queue.vue:
operation: "操作" title: "キュー"
remove-all-jobs: "すべてのジョブをクリア" remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
@ -1467,9 +1491,6 @@ desktop/views/pages/search.vue:
not-available: "検索機能はインスタンスの設定で無効になっています。" not-available: "検索機能はインスタンスの設定で無効になっています。"
not-found: "「{q}」に関する投稿は見つかりませんでした。" not-found: "「{q}」に関する投稿は見つかりませんでした。"
desktop/views/pages/share.vue:
share-with: "{name}で共有"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"

View File

@ -1,7 +1,6 @@
--- ---
meta: meta:
lang: "日本語 (関西弁)" lang: "日本語 (関西弁)"
divider: ""
common: common:
misskey: "A ⭐ of fediverse" misskey: "A ⭐ of fediverse"
about-title: "A ⭐ of fediverse." about-title: "A ⭐ of fediverse."
@ -28,13 +27,6 @@ common:
load-more: "もっとあらへんのか!" load-more: "もっとあらへんのか!"
enter-password: "パスワードを入れてや" enter-password: "パスワードを入れてや"
2fa: "二段階認証" 2fa: "二段階認証"
customize-home: "ホームをカスタマイズ"
featured-notes: "ハイライト"
dark-mode: "ダークモード"
signin: "ログイン"
signup: "新規登録"
signout: "ログアウト"
reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
got-it: "ほい" got-it: "ほい"
customization-tips: customization-tips:
title: "カスタマイズのヒント" title: "カスタマイズのヒント"
@ -64,15 +56,10 @@ common:
drive: "ドライブ" drive: "ドライブ"
messaging: "トーク" messaging: "トーク"
home: "ホーム" home: "ホーム"
deck: "デッキ"
timeline: "タイムライン" timeline: "タイムライン"
explore: "みつける" following: "フォローしとる"
following: "フォロー中"
followers: "フォロワー" followers: "フォロワー"
favorites: "お気に入り" favorites: "お気に入り"
empty-timeline-info:
follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。"
explore: "ユーザーを探索する"
weekday-short: weekday-short:
sunday: "日" sunday: "日"
monday: "月" monday: "月"
@ -118,129 +105,25 @@ common:
d: "言うときたいことは?" d: "言うときたいことは?"
e: "ここに書いてや" e: "ここに書いてや"
f: "あんさんが書くんを待っちょります..." f: "あんさんが書くんを待っちょります..."
settings: "設定"
_settings: _settings:
profile: "プロフィール" profile: "プロフィール"
notification: "通知" notification: "通知"
apps: "アプリ"
tags: "ハッシュタグ" tags: "ハッシュタグ"
mute-and-block: "ミュート/ブロック"
blocking: "ブロック" blocking: "ブロック"
security: "セキュリティ"
signin: "ログイン履歴"
password: "パスワード" password: "パスワード"
other: "その他" other: "その他"
appearance: "デザイン"
behavior: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
note-visibility: "投稿の公開範囲"
default-note-visibility: "デフォルトの公開範囲"
remember-note-visibility: "投稿の公開範囲を記憶する"
web-search-engine: "ウェブ検索エンジン"
web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
keep-cw: "CW保持"
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
use-avatar-reversi-stones: "リバーシの石にアバターを使う"
disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
disable-showing-animated-images: "アニメーション画像を再生しない"
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
always-show-nsfw: "常に閲覧注意のメディアを表示する"
always-mark-nsfw: "常にメディアを閲覧注意として投稿"
show-full-acct: "ユーザー名のホストを省略しない"
show-via: "viaを表示する"
reduce-motion: "UIの動きを減らす"
this-setting-is-this-device-only: "このデバイスのみ"
use-os-default-emojis: "OS標準の絵文字を使用"
line-width: "線の太さ"
line-width-thin: "細い"
line-width-normal: "普通"
line-width-thick: "太い"
font-size: "文字の大きさ"
font-size-x-small: "小さい"
font-size-small: "少し小さい"
font-size-medium: "普通"
font-size-large: "少し大きい"
font-size-x-large: "大きい"
deck-column-align: "デッキのカラムの配置"
deck-column-align-center: "中央"
deck-column-align-left: "左"
deck-column-align-flexible: "フレキシブル"
deck-column-width: "デッキのカラムの幅"
deck-column-width-narrow: "狭"
deck-column-width-narrower: "やや狭"
deck-column-width-normal: "普通"
deck-column-width-wider: "やや広"
deck-column-width-wide: "広"
use-shadow: "UIに影を使用"
rounded-corners: "UIの角を丸める"
circle-icons: "円形のアイコンを使用"
contrasted-acct: "ユーザー名にコントラストを付ける"
wallpaper: "壁紙"
choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
show-clock-on-header: "右上に時計を表示する"
show-reply-target: "リプライ先を表示する"
timeline: "タイムライン" timeline: "タイムライン"
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する"
remain-deleted-note: "削除された投稿を表示し続ける"
sound: "サウンド"
enable-sounds: "サウンドを有効にする"
enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
volume: "ボリューム"
test: "テスト"
update: "Misskey Update"
version: "バージョン:"
latest-version: "最新のバージョン:"
update-checking: "アップデートを確認中"
do-update: "アップデートを確認"
update-settings: "詳細設定"
no-updates: "利用可能な更新はありません"
no-updates-desc: "お使いのMisskeyは最新です。"
update-available: "新しいバージョンが利用可能です"
update-available-desc: "ページを再度読み込みすると更新が適用されます。"
advanced-settings: "高度な設定"
debug-mode: "デバッグモードを有効にする"
debug-mode-desc: "この設定はブラウザに記憶されます。"
navbar-position: "ナビゲーションバーの位置"
navbar-position-top: "上"
navbar-position-left: "左"
navbar-position-right: "右"
i-am-under-limited-internet: "私は通信を制限されている"
post-style: "投稿の表示スタイル"
post-style-standard: "標準"
post-style-smart: "スマート"
notification-position: "通知の表示"
notification-position-bottom: "下"
notification-position-top: "上"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
load-raw-images: "添付された画像を高画質で表示する"
load-remote-media: "リモートサーバーのメディアを表示する"
search: "検索" search: "検索"
delete: "削除" delete: "削除"
loading: "読み込み中" loading: "読み込み中"
ok: "おk"
cancel: "やめる"
update-available-title: "更新があんで" update-available-title: "更新があんで"
update-available: "Misskeyの新しいバージョンがあんで({newer}。現在{current}をつこてるわ)。ページを再度読み込みしたると更新が適用されるわ。" update-available: "Misskeyの新しいバージョンがあんで({newer}。現在{current}をつこてるわ)。ページを再度読み込みしたると更新が適用されるわ。"
my-token-regenerated: "あんさんのトークンが更新されたらしいわ。すまんがとりあえずサインアウトすんで。" my-token-regenerated: "あんさんのトークンが更新されたらしいわ。すまんがとりあえずサインアウトすんで。"
verified-user: "アメちゃん付きアカウント" verified-user: "アメちゃん付きアカウント"
hide-password: "パスワードを隠す"
show-password: "パスワードを表示する"
do-not-use-in-production: "開発ビルドや。本番環境で使わんといて!知らんで!" do-not-use-in-production: "開発ビルドや。本番環境で使わんといて!知らんで!"
user-suspended: "このユーザーは凍結されています。"
is-remote-user: "このユーザー情報は不正確な可能性があります。"
is-remote-post: "この投稿情報はコピーです。" is-remote-post: "この投稿情報はコピーです。"
view-on-remote: "ちゃんとした情報見せてや!" view-on-remote: "ちゃんとした情報見せてや!"
renoted-by: "{user}がRenote" renoted-by: "{user}がRenote"
no-notes: "投稿がありません"
turn-on-darkmode: "闇に飲まれる"
turn-off-darkmode: "光あれ"
error: error:
title: "問題が起こったわ" title: "問題が起こったわ"
retry: "もっぺん" retry: "もっぺん"
@ -279,7 +162,7 @@ common:
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
dev: "アプリの作成あかんかったわ。もっぺんやってみて。" dev: "アプリの作成あかんかったわ。もっぺんやってみて。"
ai-chan-kawaii: "藍ちゃめっさべっぴんさんや" ai-chan-kawaii: "藍ちゃめっさべっぴんさんや"
you: "あなた" you: "あんさん"
auth/views/form.vue: auth/views/form.vue:
share-access: "あんたのアカウントに<i>{name}</i>がアクセスしようとしてるで?ええか?" share-access: "あんたのアカウントに<i>{name}</i>がアクセスしようとしてるで?ええか?"
permission-ask: "このアプリは次の権限を要求してんで:" permission-ask: "このアプリは次の権限を要求してんで:"
@ -305,18 +188,8 @@ auth/views/index.vue:
error: "セッションが存在してへん。" error: "セッションが存在してへん。"
sign-in: "サインインしてや" sign-in: "サインインしてや"
common/views/pages/explore.vue: common/views/pages/explore.vue:
verified-users: "公式アカウント" verified-users: "アメちゃん付きアカウント"
popular-users: "人気のユーザー"
recently-updated-users: "最近投稿したユーザー"
recently-registered-users: "新規ユーザー"
popular-tags: "人気のタグ"
federated: "連合" federated: "連合"
explore: "{host}を探索"
users-info: "現在{users}ユーザーが登録されています"
common/views/components/url-preview.vue:
enable-player: "プレイヤーを開く"
common/views/components/user-list.vue:
no-users: "ユーザーがいません"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
matching: matching:
waiting-for: "{}を待っとります" waiting-for: "{}を待っとります"
@ -388,7 +261,6 @@ common/views/components/media-banner.vue:
sensitive: "見せたらあかん" sensitive: "見せたらあかん"
click-to-show: "押してみ、見せたるわ" click-to-show: "押してみ、見せたるわ"
common/views/components/theme.vue: common/views/components/theme.vue:
theme: "テーマ"
light-theme: "ナイトゲームちゃう時のテーマどないする?" light-theme: "ナイトゲームちゃう時のテーマどないする?"
dark-theme: "ナイトゲームの時のテーマどないする?" dark-theme: "ナイトゲームの時のテーマどないする?"
light-themes: "デイゲーム" light-themes: "デイゲーム"
@ -405,7 +277,6 @@ common/views/components/theme.vue:
base-theme: "この色が背景や!" base-theme: "この色が背景や!"
base-theme-light: "Light" base-theme-light: "Light"
base-theme-dark: "Dark" base-theme-dark: "Dark"
find-more-theme: "その他のテーマを入手"
theme-name: "テーマ名" theme-name: "テーマ名"
preview-created-theme: "試してみる" preview-created-theme: "試してみる"
invalid-theme: "このテーマあかんわ、なんか間違うとる" invalid-theme: "このテーマあかんわ、なんか間違うとる"
@ -427,8 +298,6 @@ common/views/components/theme.vue:
common/views/components/cw-button.vue: common/views/components/cw-button.vue:
hide: "もうええわ" hide: "もうええわ"
show: "見たいやろ?" show: "見たいやろ?"
chars: "{count}文字"
files: "{count}ファイル"
poll: "アンケート" poll: "アンケート"
common/views/components/messaging.vue: common/views/components/messaging.vue:
search-user: "ユーザーを探す" search-user: "ユーザーを探す"
@ -459,38 +328,22 @@ common/views/components/nav.vue:
develop: "開発者" develop: "開発者"
feedback: "フィードバック" feedback: "フィードバック"
common/views/components/note-menu.vue: common/views/components/note-menu.vue:
mention: "メンション"
detail: "もっと" detail: "もっと"
copy-content: "内容をコピー"
copy-link: "リンクをコピー" copy-link: "リンクをコピー"
favorite: "お気に入り" favorite: "お気に入り"
unfavorite: "お気に入りやめる" unfavorite: "お気に入りやめる"
watch: "ウォッチ"
unwatch: "ウォッチ解除"
pin: "ピン留め" pin: "ピン留め"
unpin: "ピン留めやめる" unpin: "ピン留めやめる"
delete: "ほかす" delete: "ほかす"
delete-confirm: "この投稿を削除してもええか?" delete-confirm: "この投稿を削除してもええか?"
remote: "投稿元に行ってみよか" remote: "投稿元に行ってみよか"
common/views/components/user-menu.vue: common/views/components/user-menu.vue:
mention: "メンション"
mute: "ミュート" mute: "ミュート"
unmute: "ミュート解除"
block: "ブロック" block: "ブロック"
unblock: "ブロック解除"
push-to-list: "リストに追加"
select-list: "リストを選択してください"
report-abuse: "スパムを報告"
report-abuse-detail: "どのような迷惑行為を行っていますか?"
report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
silence: "サイレンス"
unsilence: "サイレンス解除"
suspend: "凍結" suspend: "凍結"
unsuspend: "凍結解除"
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "「{}」に投票や!" vote-to: "「{}」に投票や!"
vote-count: "{}票" vote-count: "{}票"
total-users: "{}人が投票"
vote: "投票するで" vote: "投票するで"
show-result: "結果を見よか" show-result: "結果を見よか"
voted: "投票済みや" voted: "投票済みや"
@ -500,6 +353,7 @@ common/views/components/poll-editor.vue:
remove: "この選択肢を消すで" remove: "この選択肢を消すで"
add: "+選択肢を追加" add: "+選択肢を追加"
destroy: "アンケートをほかそ" destroy: "アンケートをほかそ"
day: "日"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "リアクション、どれにするんや?" choose-reaction: "リアクション、どれにするんや?"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
@ -554,11 +408,6 @@ common/views/components/stream-indicator.vue:
connected: "つないだわ" connected: "つないだわ"
common/views/components/notification-settings.vue: common/views/components/notification-settings.vue:
title: "通知" title: "通知"
mark-as-read-all-notifications: "すべての通知を既読にする"
mark-as-read-all-unread-notes: "すべての投稿を既読にする"
mark-as-read-all-talk-messages: "すべてのトークを既読にする"
auto-watch: "投稿の自動ウォッチ"
auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。"
common/views/components/integration-settings.vue: common/views/components/integration-settings.vue:
title: "サービス連携" title: "サービス連携"
connect: "つなげる" connect: "つなげる"
@ -608,7 +457,6 @@ common/views/components/profile-editor.vue:
account: "アカウント" account: "アカウント"
location: "場所" location: "場所"
description: "自己紹介" description: "自己紹介"
you-can-include-hashtags: "ハッシュタグを含めることができます。"
language: "言語" language: "言語"
birthday: "誕生日" birthday: "誕生日"
avatar: "アイコン" avatar: "アイコン"
@ -617,7 +465,6 @@ common/views/components/profile-editor.vue:
is-bot: "このアカウントはBotやで" is-bot: "このアカウントはBotやで"
is-locked: "他人のフォローは許可してからや!" is-locked: "他人のフォローは許可してからや!"
careful-bot: "Botからのフォローだけは許可制や" careful-bot: "Botからのフォローだけは許可制や"
auto-accept-followed: "フォローしているユーザーからのフォローを自動承認する"
advanced: "その他" advanced: "その他"
privacy: "プライバシーってなんや?オカンの年齢か?" privacy: "プライバシーってなんや?オカンの年齢か?"
save: "保存" save: "保存"
@ -629,23 +476,15 @@ common/views/components/profile-editor.vue:
email-verified: "このメールアドレスOKや" email-verified: "このメールアドレスOKや"
email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?" email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
export: "エクスポート" export: "エクスポート"
import: "インポート"
export-targets: export-targets:
all-notes: "すべての投稿データ"
following-list: "フォロー" following-list: "フォロー"
mute-list: "ミュート" mute-list: "ミュート"
blocking-list: "ブロック" blocking-list: "ブロック"
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。" user-lists: "リスト"
enter-password: "パスワードを入力してください" enter-password: "パスワードを入れてや"
danger-zone: "危険な設定"
delete-account: "アカウントを削除"
account-deleted: "アカウントが削除されました。データが消えるまで時間がかかる場合があります。"
common/views/components/user-list-editor.vue: common/views/components/user-list-editor.vue:
users: "ユーザー" users: "ユーザー"
rename: "リスト名を変更"
delete: "リストを削除"
remove-user: "このリストから削除"
delete-are-you-sure: "リスト「$1」を削除しますか"
deleted: "削除しました"
common/views/widgets/broadcast.vue: common/views/widgets/broadcast.vue:
fetching: "見てみるわ…" fetching: "見てみるわ…"
no-broadcasts: "お知らせはあらへんで" no-broadcasts: "お知らせはあらへんで"
@ -695,11 +534,8 @@ common/views/widgets/tips.vue:
tips-line19: "いくつかのウィンドウはブラウザの外に切り離すことができんで" tips-line19: "いくつかのウィンドウはブラウザの外に切り離すことができんで"
tips-line20: "カレンダーウィジェットのパーセンテージは、経過の割合を示してんねん" tips-line20: "カレンダーウィジェットのパーセンテージは、経過の割合を示してんねん"
tips-line21: "APIをつこてbotの開発なども行えるで" tips-line21: "APIをつこてbotの開発なども行えるで"
tips-line23: "藍かわいいよ藍"
tips-line24: "Misskeyは2014年にサービスを開始したんよ" tips-line24: "Misskeyは2014年にサービスを開始したんよ"
tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで" tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで"
common/views/pages/not-found.vue:
page-not-found: "ページが見つかりませんでした"
common/views/pages/follow.vue: common/views/pages/follow.vue:
signed-in-as: "{}としてサインイン中" signed-in-as: "{}としてサインイン中"
following: "フォローしとる" following: "フォローしとる"
@ -826,12 +662,10 @@ desktop/views/components/note-detail.vue:
location: "ここおるで:" location: "ここおるで:"
renote: "Renote" renote: "Renote"
add-reaction: "リアクション" add-reaction: "リアクション"
undo-reaction: "リアクション解除"
desktop/views/components/note.vue: desktop/views/components/note.vue:
reply: "返す" reply: "返す"
renote: "Renote" renote: "Renote"
add-reaction: "リアクション" add-reaction: "リアクション"
undo-reaction: "リアクション解除"
detail: "もっと" detail: "もっと"
private: "この投稿は見せられへんわ" private: "この投稿は見せられへんわ"
deleted: "この投稿なんか無くなってもうたわ" deleted: "この投稿なんか無くなってもうたわ"
@ -859,7 +693,6 @@ desktop/views/components/post-form.vue:
attach-media-from-local: "PCからメディア持ってくる" attach-media-from-local: "PCからメディア持ってくる"
attach-media-from-drive: "ドライブからメディア持ってくる" attach-media-from-drive: "ドライブからメディア持ってくる"
attach-cancel: "くっつけるのやめよか" attach-cancel: "くっつけるのやめよか"
insert-a-kao: "v('ω')v"
create-poll: "アンケートを作成" create-poll: "アンケートを作成"
text-remain: "残り{}文字" text-remain: "残り{}文字"
recent-tags: "最近のタグ" recent-tags: "最近のタグ"
@ -910,8 +743,8 @@ desktop/views/components/settings.2fa.vue:
failed: "なんか設定に失敗したで。トークンを間違えとらんか確認してや。" failed: "なんか設定に失敗したで。トークンを間違えとらんか確認してや。"
info: "次のサインインからは、パスワードに加えてデバイスに出とるトークンを入力してな。" info: "次のサインインからは、パスワードに加えてデバイスに出とるトークンを入力してな。"
common/views/components/media-image.vue: common/views/components/media-image.vue:
sensitive: "閲覧注意" sensitive: "ちょっと見せられへんわ"
click-to-show: "クリックして表示" click-to-show: "クリックして見せるで"
common/views/components/api-settings.vue: common/views/components/api-settings.vue:
intro: "API使うんやったらこのトークンを「i」っちゅうパラメータにくっつけてリクエストできるで。" intro: "API使うんやったらこのトークンを「i」っちゅうパラメータにくっつけてリクエストできるで。"
caution: "アカウント勝手にいじられるかも知れんから、このトークンは教えたらあかんし、アプリにも書いたらあかんで(これはフリちゃうで)" caution: "アカウント勝手にいじられるかも知れんから、このトークンは教えたらあかんし、アプリにも書いたらあかんで(これはフリちゃうで)"
@ -950,16 +783,13 @@ common/views/components/password-settings.vue:
enter-new-password-again: "もっぺん入れてや" enter-new-password-again: "もっぺん入れてや"
not-match: "パスワードがおうとらん" not-match: "パスワードがおうとらん"
changed: "パスワード変えたわ" changed: "パスワード変えたわ"
failed: "パスワード変更に失敗しました"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
private: "この投稿は見せられへんわ" private: "この投稿は見せられへんわ"
deleted: "この投稿なんか無くなってもうたわ" deleted: "この投稿なんか無くなってもうたわ"
media-count: "{}つのメディア" media-count: "{}つのメディア"
poll: "アンケート" poll: "アンケート"
desktop/views/components/settings.tags.vue: desktop/views/components/settings.tags.vue:
title: "タグ" add: "増やす"
query: "クエリ (省略可)"
add: "追加"
save: "保存" save: "保存"
desktop/views/components/taskmanager.vue: desktop/views/components/taskmanager.vue:
title: "タスクマネージャ" title: "タスクマネージャ"
@ -1021,8 +851,6 @@ admin/views/index.vue:
federation: "連合" federation: "連合"
announcements: "知っといてや" announcements: "知っといてや"
hashtags: "ハッシュタグ" hashtags: "ハッシュタグ"
abuse: "スパム報告"
queue: "ジョブキュー"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
@ -1034,12 +862,8 @@ admin/views/dashboard.vue:
federated: "連合" federated: "連合"
admin/views/queue.vue: admin/views/queue.vue:
operation: "操作" operation: "操作"
remove-all-jobs: "すべてのジョブをクリア"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "スパム報告" details: "もっと"
target: "対象"
reporter: "報告者"
details: "詳細"
remove-report: "削除" remove-report: "削除"
admin/views/instance.vue: admin/views/instance.vue:
instance: "インスタンス" instance: "インスタンス"
@ -1047,7 +871,6 @@ admin/views/instance.vue:
instance-description: "インスタンスの紹介" instance-description: "インスタンスの紹介"
host: "ホスト" host: "ホスト"
banner-url: "バナー画像URL" banner-url: "バナー画像URL"
error-image-url: "エラー画像URL"
languages: "インスタンスの対象言語" languages: "インスタンスの対象言語"
languages-desc: "スペースで区切って複数設定できるで。" languages-desc: "スペースで区切って複数設定できるで。"
maintainer-config: "管理者情報" maintainer-config: "管理者情報"
@ -1087,8 +910,6 @@ admin/views/instance.vue:
max-note-text-length: "投稿の最大文字数" max-note-text-length: "投稿の最大文字数"
disable-registration: "ユーザー登録の受付を止める" disable-registration: "ユーザー登録の受付を止める"
disable-local-timeline: "ローカルタイムラインを使えんようにする" disable-local-timeline: "ローカルタイムラインを使えんようにする"
disable-global-timeline: "グローバルタイムラインを無効にする"
disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
invite: "来てや" invite: "来てや"
save: "保存" save: "保存"
saved: "保存したで!" saved: "保存したで!"
@ -1106,15 +927,8 @@ admin/views/instance.vue:
smtp-secure-info: "STARTTLS使用時はオフにします。" smtp-secure-info: "STARTTLS使用時はオフにします。"
smtp-host: "SMTPホスト" smtp-host: "SMTPホスト"
smtp-port: "SMTPポート" smtp-port: "SMTPポート"
smtp-auth: "SMTP認証を行う"
smtp-user: "SMTPユーザー" smtp-user: "SMTPユーザー"
smtp-pass: "SMTPパスワード" smtp-pass: "SMTPパスワード"
serviceworker-config: "ServiceWorker"
enable-serviceworker: "ServiceWorkerを有効にする"
serviceworker-info: "プッシュ通知を行うには有効する必要があります。"
vapid-publickey: "VAPID公開鍵"
vapid-privatekey: "VAPID秘密鍵"
vapid-info: "ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります。シェルで次のようにします:"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@ -1133,7 +947,6 @@ admin/views/charts.vue:
notes-total: "投稿の積算" notes-total: "投稿の積算"
users: "ユーザーの増減" users: "ユーザーの増減"
users-total: "ユーザーの積算" users-total: "ユーザーの積算"
active-users: "アクティブユーザー数"
drive: "ドライブ使用量の増減" drive: "ドライブ使用量の増減"
drive-total: "ドライブ使用量の積算" drive-total: "ドライブ使用量の積算"
drive-files: "ドライブのファイル数の増減" drive-files: "ドライブのファイル数の増減"
@ -1143,168 +956,61 @@ admin/views/charts.vue:
network-usage: "通信量" network-usage: "通信量"
admin/views/drive.vue: admin/views/drive.vue:
operation: "操作" operation: "操作"
fileid-or-url: "ファイルIDまたはファイルURL"
file-not-found: "ファイルが見つかりません"
lookup: "照会" lookup: "照会"
sort:
title: "ソート"
createdAtAsc: "アップロード日時が古い順"
createdAtDesc: "アップロード日時が新しい順"
sizeAsc: "サイズが小さい順"
sizeDesc: "サイズが大きい順"
origin: origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル" local: "ローカル"
remote: "リモート"
delete: "削除" delete: "削除"
deleted: "削除しました" mark-as-sensitive: "見たらあかん感じにしとく"
mark-as-sensitive: "閲覧注意に設定" unmark-as-sensitive: "やっぱ見せたるわ"
unmark-as-sensitive: "閲覧注意を解除"
marked-as-sensitive: "閲覧注意に設定しました"
unmarked-as-sensitive: "閲覧注意を解除しました"
admin/views/users.vue: admin/views/users.vue:
operation: "操作" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID" username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つからへん!" user-not-found: "ユーザーが見つからへん!"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password} 」やで" password-updated: "パスワードは現在「{password} 」やで"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました"
unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました"
make-silence: "サイレンス"
unmake-silence: "サイレンスの解除"
verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
users: users:
title: "ユーザー" title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
state: state:
title: "状態"
all: "すべて" all: "すべて"
admin: "管理者"
moderator: "モデレーター" moderator: "モデレーター"
adminOrModerator: "管理者+モデレーター" verified: "アメちゃん付きアカウント"
verified: "公式アカウント"
silenced: "サイレンス済み"
suspended: "凍結済み"
origin: origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル" local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"
add: "登録"
added: "モデレーターを登録しました"
remove: "解除"
removed: "モデレーター登録を解除しました"
admin/views/emoji.vue: admin/views/emoji.vue:
add-emoji: add-emoji:
title: "絵文字の登録" add: "増やす"
name: "絵文字名"
name-desc: "a~z 0~9 _ の文字が使えます。"
aliases: "エイリアス"
aliases-desc: "スペースで区切って複数設定できます。"
url: "絵文字画像URL"
add: "追加"
info: "50KB以下のPNG画像をおすすめします。"
added: "絵文字を登録しました"
emojis: emojis:
title: "絵文字一覧"
update: "更新"
remove: "削除" remove: "削除"
updated: "更新しました"
remove-emoji:
are-you-sure: "「$1」を削除しますか"
removed: "削除しました"
admin/views/announcements.vue: admin/views/announcements.vue:
announcements: "お知らせ" announcements: "知っときや"
save: "保存" save: "保存"
remove: "削除" remove: "削除"
add: "追加" add: "増やす"
title: "タイトル" saved: "保存したで!"
text: "内容"
saved: "保存しました"
_remove:
are-you-sure: "「$1」を削除しますか"
removed: "削除しました"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"
admin/views/federation.vue: admin/views/federation.vue:
federation: "連合" federation: "連合"
host: "ホスト" host: "ホスト"
notes: "投稿" notes: "投稿"
users: "ユーザー" users: "ユーザー"
following: "フォロー" following: "フォローしとる"
followers: "フォロワー" followers: "フォロワー"
status: "ステータス" status: "ステータス"
latest-request-sent-at: "直近のリクエスト送信"
latest-request-received-at: "直近のリクエスト受信"
remove-all-following: "フォローを全解除"
remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
block: "ブロック" block: "ブロック"
marked-as-closed: "閉鎖されているとマーク"
lookup: "照会" lookup: "照会"
instances: "インスタンス" instances: "インスタンス"
instance-not-registered: "そのインスタンスは登録されていません"
sort: "ソート"
sorts:
caughtAtAsc: "登録日時が古い順"
caughtAtDesc: "登録日時が新しい順"
lastCommunicatedAtAsc: "最後にやり取りした日時が古い順"
lastCommunicatedAtDesc: "最後にやり取りした日時が新しい順"
notesAsc: "投稿が少ない順"
notesDesc: "投稿が多い順"
usersAsc: "ユーザーが少ない順"
usersDesc: "ユーザーが多い順"
followingAsc: "フォローが少ない順"
followingDesc: "フォローが多い順"
followersAsc: "フォロワーが少ない順"
followersDesc: "フォロワーが多い順"
driveUsageAsc: "ドライブ使用量が少ない順"
driveUsageDesc: "ドライブ使用量が多い順"
driveFilesAsc: "ドライブのファイル数が少ない順"
driveFilesDesc: "ドライブのファイル数が多い順"
state: "状態"
states: states:
all: "すべて" all: "すべて"
blocked: "ブロック" blocked: "ブロック"
not-responding: "応答なし"
marked-as-closed: "閉鎖とマーク済み"
result-is-truncated: "上位{n}件を表示しています。"
charts: "チャート" charts: "チャート"
chart-srcs: chart-srcs:
requests: "リクエスト" requests: "リクエスト"
users: "ユーザーの増減" users: "ユーザーの増減"
users-total: "ユーザーの積算" users-total: "ユーザーの積算"
notes: "投稿の増減"
notes-total: "投稿の積算" notes-total: "投稿の積算"
ff: "フォロー/フォロワーの増減"
ff-total: "フォロー/フォロワーの積算"
drive-usage: "ドライブ使用量の増減" drive-usage: "ドライブ使用量の増減"
drive-usage-total: "ドライブ使用量の積算" drive-usage-total: "ドライブ使用量の積算"
drive-files: "ドライブファイル数の増減"
drive-files-total: "ドライブファイル数の積算"
chart-spans: chart-spans:
hour: "1時間ごと" hour: "1時間ごと"
day: "1日ごと" day: "1日ごと"
@ -1327,11 +1033,6 @@ desktop/views/pages/selectdrive.vue:
upload: "PCからドライブにファイル上げる" upload: "PCからドライブにファイル上げる"
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "検索機能は使えへんわ。管理者がそう言うとる。" not-available: "検索機能は使えへんわ。管理者がそう言うとる。"
not-found: "「{q}」に関する投稿は見つかりませんでした。"
desktop/views/pages/share.vue:
share-with: "{name}で共有"
desktop/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
users: "ユーザー" users: "ユーザー"
add-user: "ユーザー増やす" add-user: "ユーザー増やす"
@ -1353,17 +1054,15 @@ desktop/views/pages/user/user.header.vue:
following: "フォロー" following: "フォロー"
followers: "フォロワー" followers: "フォロワー"
is-bot: "このアカウントはBotや" is-bot: "このアカウントはBotや"
no-description: "自己紹介はありません"
years-old: "{age}歳" years-old: "{age}歳"
year: "年" year: "年"
month: "月" month: "月"
day: "日" day: "日"
follows-you: "フォローされています" follows-you: "フォローされとるで"
desktop/views/pages/user/user.timeline.vue: desktop/views/pages/user/user.timeline.vue:
default: "投稿" default: "投稿"
with-replies: "投稿と返信" with-replies: "投稿と返信"
with-media: "メディア" with-media: "メディア"
my-posts: "私の投稿"
desktop/views/widgets/messaging.vue: desktop/views/widgets/messaging.vue:
title: "メッセージ" title: "メッセージ"
desktop/views/widgets/notifications.vue: desktop/views/widgets/notifications.vue:
@ -1396,7 +1095,6 @@ mobile/views/components/drive.vue:
prompt: "何すんの?(数字を入れてや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>" prompt: "何すんの?(数字を入れてや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!" deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!"
folder-name: "フォルダー名" folder-name: "フォルダー名"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "このURLのファイルをアップロードしたいねん" url-prompt: "このURLのファイルをアップロードしたいねん"
uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。" uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。"
mobile/views/components/drive-file-chooser.vue: mobile/views/components/drive-file-chooser.vue:
@ -1492,8 +1190,7 @@ mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿はあらへんかった。" no-posts-found: "ハッシュタグ「{q}」が付けられた投稿はあらへんかった。"
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。" add-widget: "増やす"
add-widget: "追加"
customization-tips: "カスタマイズのヒント" customization-tips: "カスタマイズのヒント"
mobile/views/pages/widgets/activity.vue: mobile/views/pages/widgets/activity.vue:
activity: "やっとること" activity: "やっとること"
@ -1531,7 +1228,7 @@ mobile/views/pages/user/home.vue:
activity: "やっとること" activity: "やっとること"
keywords: "キーワード" keywords: "キーワード"
domains: "よく出るドメイン" domains: "よく出るドメイン"
frequently-replied-users: "よ話すユーザー" frequently-replied-users: "よ話すツレ"
followers-you-know: "知っとるフォロワー" followers-you-know: "知っとるフォロワー"
last-used-at: "最後いつ来た?" last-used-at: "最後いつ来た?"
mobile/views/pages/user/home.photos.vue: mobile/views/pages/user/home.photos.vue:
@ -1547,7 +1244,6 @@ deck:
direct: "ダイレクト投稿" direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
select-list: "リストを選択してください"
swap-left: "左に移動や!" swap-left: "左に移動や!"
swap-right: "右に移動や!" swap-right: "右に移動や!"
swap-up: "上に移動や!" swap-up: "上に移動や!"
@ -1557,14 +1253,11 @@ deck:
rename: "名前を変えるで" rename: "名前を変えるで"
stack-left: "左に重ねんで!" stack-left: "左に重ねんで!"
pop-right: "右に出すで!" pop-right: "右に出すで!"
disabled-timeline:
title: "無効化されたタイムライン"
description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。"
deck/deck.tl-column.vue: deck/deck.tl-column.vue:
is-media-only: "メディア投稿だけや" is-media-only: "メディア投稿だけや"
edit: "オプション" edit: "オプション"
deck/deck.user-column.vue: deck/deck.user-column.vue:
follows-you: "フォローされています" follows-you: "フォローされとるで"
posts: "投稿" posts: "投稿"
following: "フォロー" following: "フォロー"
followers: "フォロワー" followers: "フォロワー"

View File

@ -1,7 +1,6 @@
--- ---
meta: meta:
lang: "한국어" lang: "한국어"
divider: ""
common: common:
misskey: "연합우주의 ⭐" misskey: "연합우주의 ⭐"
about-title: "연합우주의 ⭐." about-title: "연합우주의 ⭐."
@ -490,16 +489,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "\"{}\"에 투표하기" vote-to: "\"{}\"에 투표하기"
vote-count: "{}표" vote-count: "{}표"
total-users: "{}명이 투표" total-votes: "{}표"
vote: "투표하기" vote: "투표하기"
show-result: "결과 보기" show-result: "결과 보기"
voted: "투표함" voted: "투표함"
closed: "종료됨"
remaining-days: "종료까지 앞으로 {d}일 {h}시간"
remaining-hours: "종료까지 앞으로 {h}시간 {m}분"
remaining-minutes: "종료까지 앞으로 {m}분 {s}초"
remaining-seconds: "종료까지 앞으로 {s}초"
common/views/components/poll-editor.vue: common/views/components/poll-editor.vue:
no-only-one-choice: "투표에는 선택지가 최소한 두 개 필요합니다" no-only-one-choice: "투표에는 선택지가 최소한 두 개 필요합니다"
choice-n: "선택지 {}" choice-n: "선택지 {}"
remove: "이 선택지를 제거" remove: "이 선택지를 제거"
add: "+선택지 추가" add: "+선택지 추가"
destroy: "투표 제거" destroy: "투표 제거"
multiple: "복수 응답 가능"
expiration: "기한"
infinite: "무기한"
at: "일시 지정"
after: "기간 지정"
no-more: "더 이상 추가할 수 없습니다"
deadline-date: "기한"
deadline-time: "시간"
interval: "기간"
unit: "단위"
second: "초"
minute: "분"
hour: "시간"
day: "일"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "반응 선택" choose-reaction: "반응 선택"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
@ -629,12 +647,16 @@ common/views/components/profile-editor.vue:
email-verified: "매일 주소가 확인되었습니다" email-verified: "매일 주소가 확인되었습니다"
email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다." email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
export: "내보내기" export: "내보내기"
import: "가져오기"
export-and-import: "내보내기와 가져오기"
export-targets: export-targets:
all-notes: "모든 글 데이터" all-notes: "모든 글 데이터"
following-list: "팔로잉" following-list: "팔로잉"
mute-list: "뮤트" mute-list: "뮤트"
blocking-list: "차단" blocking-list: "차단"
user-lists: "리스트"
export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다." export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
import-requested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
enter-password: "비밀번호를 입력하여 주십시오" enter-password: "비밀번호를 입력하여 주십시오"
danger-zone: "위험한 설정" danger-zone: "위험한 설정"
delete-account: "계정 삭제" delete-account: "계정 삭제"
@ -1023,6 +1045,7 @@ admin/views/index.vue:
hashtags: "해시태그" hashtags: "해시태그"
abuse: "스팸 신고" abuse: "스팸 신고"
queue: "작업 대기열" queue: "작업 대기열"
logs: "로그"
back-to-misskey: "Misskey로 돌아가기" back-to-misskey: "Misskey로 돌아가기"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "대시보드" dashboard: "대시보드"
@ -1328,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다." not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다."
not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다." not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
desktop/views/pages/share.vue:
share-with: "{name}(으)로 공유"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다." no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
--- ---
meta: meta:
lang: "中文(简体)" lang: "中文(简体)"
divider: ""
common: common:
misskey: "Fediverse中的一颗⭐" misskey: "Fediverse中的一颗⭐"
about-title: "Fediverse中的一颗⭐" about-title: "Fediverse中的一颗⭐"
@ -490,16 +489,35 @@ common/views/components/user-menu.vue:
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "为\"{}\"投票" vote-to: "为\"{}\"投票"
vote-count: "{}票" vote-count: "{}票"
total-users: "{} 人投票" total-votes: "总票数{}"
vote: "投票" vote: "投票"
show-result: "显示结果" show-result: "显示结果"
voted: "已投票" voted: "已投票"
closed: "已截止"
remaining-days: "{d}天{h}小时后截止"
remaining-hours: "{h}小时{m}分后截止"
remaining-minutes: "{m}分{s}秒后截止"
remaining-seconds: "{s}秒后截止"
common/views/components/poll-editor.vue: common/views/components/poll-editor.vue:
no-only-one-choice: "至少选择两个选项" no-only-one-choice: "至少选择两个选项"
choice-n: "选择{}" choice-n: "选择{}"
remove: "删除选项" remove: "删除选项"
add: "+添加一个选项" add: "+添加一个选项"
destroy: "放弃投票" destroy: "放弃投票"
multiple: "允许多个投票"
expiration: "截止时间"
infinite: "永久"
at: "指定日期"
after: "指定时间"
no-more: "最多只能添加十个回答"
deadline-date: "日期"
deadline-time: "时间"
interval: "时长"
unit: "单位"
second: "秒"
minute: "分"
hour: "小时"
day: "日"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "选择回应" choose-reaction: "选择回应"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
@ -629,12 +647,16 @@ common/views/components/profile-editor.vue:
email-verified: "电子邮件地址已验证" email-verified: "电子邮件地址已验证"
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。" email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
export: "导出" export: "导出"
import: "导入"
export-and-import: "导出/导入"
export-targets: export-targets:
all-notes: "所有发帖" all-notes: "所有发帖"
following-list: "关注列表" following-list: "关注列表"
mute-list: "屏蔽列表" mute-list: "屏蔽列表"
blocking-list: "黑名单" blocking-list: "黑名单"
user-lists: "列表"
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。" export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
import-requested: "导入请求已提交。这可能需要花一点时间。"
enter-password: "请输入您的密码" enter-password: "请输入您的密码"
danger-zone: "危险选项" danger-zone: "危险选项"
delete-account: "删除帐户" delete-account: "删除帐户"
@ -1023,6 +1045,7 @@ admin/views/index.vue:
hashtags: "标签" hashtags: "标签"
abuse: "举报垃圾信息" abuse: "举报垃圾信息"
queue: "作业队列" queue: "作业队列"
logs: "登录"
back-to-misskey: "返回 Misskey" back-to-misskey: "返回 Misskey"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Dashboard" dashboard: "Dashboard"
@ -1328,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "在此实例的设置中关闭搜索功能。" not-available: "在此实例的设置中关闭搜索功能。"
not-found: "没有找到“{q}”的帖子" not-found: "没有找到“{q}”的帖子"
desktop/views/pages/share.vue:
share-with: "共享{name}"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "没有找到带有主题标签“{q}”的帖子" no-posts-found: "没有找到带有主题标签“{q}”的帖子"
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:

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

View File

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

View File

@ -4,7 +4,7 @@
<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template> <template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
<section class="fit-top"> <section class="fit-top">
<ui-horizon-group inputs> <ui-horizon-group inputs>
<ui-input v-model="domain"> <ui-input v-model="domain" debounce>
<span>{{ $t('domain') }}</span> <span>{{ $t('domain') }}</span>
</ui-input> </ui-input>
<ui-select v-model="level"> <ui-select v-model="level">
@ -20,7 +20,10 @@
<div class="nqjzuvev"> <div class="nqjzuvev">
<code v-for="log in logs" :key="log._id" :class="log.level"> <code v-for="log in logs" :key="log._id" :class="log.level">
<mk-time :time="log.createdAt"/> [{{ log.domain.join(' ') }}] {{ log.message }} <details>
<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
</details>
</code> </code>
</div> </div>
</section> </section>
@ -32,10 +35,15 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { faStream } from '@fortawesome/free-solid-svg-icons'; import { faStream } from '@fortawesome/free-solid-svg-icons';
import VueJsonPretty from 'vue-json-pretty';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/logs.vue'), i18n: i18n('admin/views/logs.vue'),
components: {
VueJsonPretty
},
data() { data() {
return { return {
logs: [], logs: [],
@ -66,9 +74,9 @@ export default Vue.extend({
this.$root.api('admin/logs', { this.$root.api('admin/logs', {
level: this.level === 'all' ? null : this.level, level: this.level === 'all' ? null : this.level,
domain: this.domain === '' ? null : this.domain, domain: this.domain === '' ? null : this.domain,
limit: 50 limit: 100
}).then(logs => { }).then(logs => {
this.logs = logs; this.logs = logs.reverse();
}); });
} }
} }
@ -77,11 +85,10 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.nqjzuvev .nqjzuvev
white-space nowrap
overflow auto
padding 8px padding 8px
background #000 background #000
color #fff color #fff
font-size 14px
> code > code
display block display block

View File

@ -1,7 +1,37 @@
<template> <template>
<div> <div>
<ui-card> <ui-card>
<template #title>{{ $t('operation') }}</template> <template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
<section class="wptihjuy">
<header><fa :icon="faPaperPlane"/> Deliver</header>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
<span>Waiting</span>
</ui-input>
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
<span>Active</span>
</ui-input>
</ui-horizon-group>
<div ref="deliverChart" class="chart"></div>
</section>
<section class="wptihjuy">
<header><fa :icon="faInbox"/> Inbox</header>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
<span>Waiting</span>
</ui-input>
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
<span>Delayed</span>
</ui-input>
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
<span>Active</span>
</ui-input>
</ui-horizon-group>
<div ref="inboxChart" class="chart"></div>
</section>
<section> <section>
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button> <ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
</section> </section>
@ -12,15 +42,128 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2';
import { faTasks, faInbox } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/queue.vue'), i18n: i18n('admin/views/queue.vue'),
data() { data() {
return { return {
stats: [],
deliverChart: null,
inboxChart: null,
faTasks, faPaperPlane, faInbox
}; };
}, },
computed: {
latestStats(): any {
return this.stats[this.stats.length - 1];
}
},
watch: {
stats(stats) {
this.inboxChart.updateSeries([{
name: 'Active',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
name: 'Waiting',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
name: 'Delayed',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.deliverChart.updateSeries([{
name: 'Active',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
name: 'Waiting',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
name: 'Delayed',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
mounted() {
const chartOpts = {
chart: {
type: 'area',
height: 200,
animations: {
dynamicAnimation: {
enabled: false
}
},
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
},
stroke: {
curve: 'straight',
width: 2
},
tooltip: {
enabled: false
},
legend: {
labels: {
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
},
},
series: [] as any,
colors: ['#00BCD4', '#FFEB3B', '#e53935'],
xaxis: {
type: 'numeric',
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
show: false,
min: 0,
}
};
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
this.inboxChart.render();
this.deliverChart.render();
const connection = this.$root.stream.useSharedConnection('queueStats');
connection.on('stats', this.onStats);
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 100
});
this.$once('hook:beforeDestroy', () => {
connection.dispose();
this.inboxChart.destroy();
this.deliverChart.destroy();
});
},
methods: { methods: {
async removeAllJobs() { async removeAllJobs() {
const process = async () => { const process = async () => {
@ -38,6 +181,24 @@ export default Vue.extend({
}); });
}); });
}, },
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
} }
}); });
</script> </script>
<style lang="stylus" scoped>
.wptihjuy
> .chart
min-height 200px !important
</style>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<template> <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"> <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 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> <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> </a>
</template> </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> <style lang="stylus" scoped>
.a .a
display block display block

View File

@ -5,17 +5,17 @@
<div style="overflow: hidden; line-height: 28px;"> <div style="overflow: hidden; line-height: 28px;">
<p class="turn" v-if="!iAmPlayer && !game.isEnded"> <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/> <mk-ellipsis/>
</p> </p>
<p class="turn" v-if="logPos != logs.length"> <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>
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></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="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"> <p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner"> <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> <span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
</template> </template>
<template v-else>{{ $t('@.reversi.drawn') }}</template> <template v-else>{{ $t('@.reversi.drawn') }}</template>

View File

@ -12,21 +12,54 @@
</li> </li>
</ul> </ul>
<button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button> <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')"> <button class="destroy" @click="destroy" :title="$t('destroy')">
<fa icon="times"/> <fa icon="times"/>
</button> </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> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import * as moment from 'moment';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import { erase } from '../../../../../prelude/array'; import { erase } from '../../../../../prelude/array';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/poll-editor.vue'), i18n: i18n('common/views/components/poll-editor.vue'),
data() { data() {
return { return {
choices: ['', ''] choices: ['', ''],
multiple: false,
expiration: 'infinite',
atDate: moment().add(1, 'day').toISOString().split('T')[0],
atTime: '00:00',
after: 0,
unit: 'second'
}; };
}, },
watch: { watch: {
@ -55,15 +88,46 @@ export default Vue.extend({
}, },
get() { 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 { return {
choices: erase('', this.choices) choices: erase('', this.choices),
} multiple: this.multiple,
...(
this.expiration === 'at' ? { expiresAt: at() } :
this.expiration === 'after' ? { expiredAfter: after() } : {})
};
}, },
set(data) { set(data) {
if (data.choices.length == 0) return; if (data.choices.length == 0) return;
this.choices = data.choices; this.choices = data.choices;
if (data.choices.length == 1) this.choices = this.choices.concat(''); 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 margin 8px 0 0 0
vertical-align top vertical-align top
color var(--primary) color var(--primary)
z-index 1
> .destroy > .destroy
position absolute position absolute
@ -142,4 +207,23 @@ export default Vue.extend({
&:active &:active
color var(--primaryDarken30) 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> </style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="mk-poll" :data-is-voted="isVoted"> <div class="mk-poll" :data-done="closed || isVoted">
<ul> <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) : ''"> <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> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span> <span>
<template v-if="choice.isVoted"><fa icon="check"/></template> <template v-if="choice.isVoted"><fa icon="check"/></template>
<mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/> <mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/>
@ -10,11 +10,13 @@
</span> </span>
</li> </li>
</ul> </ul>
<p v-if="total > 0"> <p>
<span>{{ $t('total-users').replace('{}', total) }}</span> <span>{{ $t('total-votes').replace('{}', total) }}</span>
<span></span> <span> · </span>
<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a> <a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
<span v-if="isVoted">{{ $t('voted') }}</span> <span v-if="isVoted">{{ $t('voted') }}</span>
<span v-else-if="closed">{{ $t('closed') }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span>
</p> </p>
</div> </div>
</template> </template>
@ -28,6 +30,7 @@ export default Vue.extend({
props: ['note'], props: ['note'],
data() { data() {
return { return {
remaining: -1,
showResult: false showResult: false
}; };
}, },
@ -38,19 +41,43 @@ export default Vue.extend({
total(): number { total(): number {
return sum(this.poll.choices.map(x => x.votes)); 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 { isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted); return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
} }
}, },
created() { created() {
this.showResult = this.isVoted; 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: { methods: {
toggleShowResult() { toggleShowResult() {
this.showResult = !this.showResult; this.showResult = !this.showResult;
}, },
vote(id) { 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', { this.$root.api('notes/polls/vote', {
noteId: this.note.id, noteId: this.note.id,
choice: id choice: id
@ -61,7 +88,7 @@ export default Vue.extend({
Vue.set(c, 'isVoted', true); 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 a
color inherit color inherit
&[data-is-voted] &[data-done]
> ul > li > ul > li
cursor default cursor default

View File

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

View File

@ -179,6 +179,7 @@
<x-mute-and-block/> <x-mute-and-block/>
</template> </template>
<!--
<template v-if="page == null || page == 'apps'"> <template v-if="page == null || page == 'apps'">
<ui-card> <ui-card>
<template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template> <template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template>
@ -187,6 +188,7 @@
</section> </section>
</ui-card> </ui-card>
</template> </template>
-->
<template v-if="page == null || page == 'security'"> <template v-if="page == null || page == 'security'">
<ui-card> <ui-card>
@ -203,12 +205,14 @@
</section> </section>
</ui-card> </ui-card>
<!--
<ui-card> <ui-card>
<template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template> <template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template>
<section> <section>
<x-signins/> <x-signins/>
</section> </section>
</ui-card> </ui-card>
-->
</template> </template>
<template v-if="page == null || page == 'api'"> <template v-if="page == null || page == 'api'">

View File

@ -386,7 +386,7 @@ export default Vue.extend({
height: 50px; height: 50px;
background-color: #83D8FF; background-color: #83D8FF;
border-radius: 90px - 6; 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 { &:before {
content: 'Light'; content: 'Light';
@ -418,14 +418,14 @@ export default Vue.extend({
background-color: #FFCF96; background-color: #FFCF96;
border-radius: 50px; border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3); 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); transform: rotate(-45deg);
.crater { .crater {
position: absolute; position: absolute;
background-color: #E8CDA5; background-color: #E8CDA5;
opacity: 0; opacity: 0;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out !important;
border-radius: 100%; border-radius: 100%;
} }
@ -454,7 +454,7 @@ export default Vue.extend({
.star { .star {
position: absolute; position: absolute;
background-color: #ffffff; 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%; border-radius: 50%;
} }
@ -486,7 +486,7 @@ export default Vue.extend({
.star--5, .star--5,
.star--6 { .star--6 {
opacity: 0; 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 { .star--4 {
@ -559,13 +559,13 @@ export default Vue.extend({
transform: translate3d(0,0,0); transform: translate3d(0,0,0);
} }
.star--4 { .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 { .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 { .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

@ -9,7 +9,22 @@
<span class="title" ref="title"><slot name="title"></slot></span> <span class="title" ref="title"><slot name="title"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'"> <template v-if="type != 'file'">
<input ref="input" <input v-if="debounce" ref="input"
v-debounce="500"
:type="type"
v-model.lazy="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
>
<input v-else ref="input"
:type="type" :type="type"
v-model="v" v-model="v"
:disabled="disabled" :disabled="disabled"
@ -51,9 +66,13 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import debounce from 'v-debounce';
const getPasswordStrength = require('syuilo-password-strength'); const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({ export default Vue.extend({
directives: {
debounce
},
inject: { inject: {
horizonGrouped: { horizonGrouped: {
default: false default: false
@ -98,6 +117,9 @@ export default Vue.extend({
spellcheck: { spellcheck: {
required: false required: false
}, },
debounce: {
required: false
},
withPasswordMeter: { withPasswordMeter: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -344,6 +366,9 @@ root(fill)
&[type='file'] &[type='file']
display none display none
&[type='number']
text-align right
> .prefix > .prefix
> .suffix > .suffix
display block display block

View File

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

View File

@ -6,7 +6,7 @@
<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb"> <div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
<div ref="chart" class="chart"></div> <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> </div>
</x-column> </x-column>
</template> </template>

View File

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

View File

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

View File

@ -31,3 +31,4 @@ Vue.component('mkw-version', wVersion);
Vue.component('mkw-hashtags', wHashtags); Vue.component('mkw-hashtags', wHashtags);
Vue.component('mkw-instance', wInstance); Vue.component('mkw-instance', wInstance);
Vue.component('mkw-post-form', wPostForm); 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.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
<div ref="in"></div>
</div>
<div>
<b>Out</b>
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | 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.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.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

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

View File

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

View File

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

View File

@ -115,6 +115,8 @@ export default Vue.extend({
uploadings: [], uploadings: [],
poll: false, poll: false,
pollChoices: [], pollChoices: [],
pollMultiple: false,
pollExpiration: [],
useCw: false, useCw: false,
cw: null, cw: null,
geo: null, geo: null,
@ -295,7 +297,10 @@ export default Vue.extend({
}, },
onPollUpdate() { 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(); this.saveDraft();
}, },

View File

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

View File

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

View File

@ -16,11 +16,11 @@ import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update'; import checkForUpdate from './common/scripts/check-for-update';
import MiOS from './mios'; import MiOS from './mios';
import { version, codename, lang, locale } from './config'; import { version, codename, lang, locale } from './config';
import { builtinThemes, lightTheme, applyTheme } from './theme'; import { builtinThemes, applyTheme, darkTheme } from './theme';
import Dialog from './common/views/components/dialog.vue'; import Dialog from './common/views/components/dialog.vue';
if (localStorage.getItem('theme') == null) { if (localStorage.getItem('theme') == null) {
applyTheme(lightTheme); applyTheme(darkTheme);
} }
//#region FontAwesome //#region FontAwesome
@ -389,7 +389,7 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
}); });
//#endregion //#endregion
// Reapply current theme /*// Reapply current theme
try { try {
const themeName = os.store.state.device.darkmode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme; const themeName = os.store.state.device.darkmode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme;
const themes = os.store.state.device.themes.concat(builtinThemes); const themes = os.store.state.device.themes.concat(builtinThemes);
@ -399,7 +399,7 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
} }
} catch (e) { } catch (e) {
console.log(`Cannot reapply theme. ${e}`); console.log(`Cannot reapply theme. ${e}`);
} }*/
//#region line width //#region line width
document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`); document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`);

View File

@ -172,7 +172,11 @@ export default class MiOS extends EventEmitter {
callback(); callback();
// Init service worker // 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 * Register service worker
*/ */
@autobind @autobind
private registerSw() { private registerSw(swPublickey) {
// Check whether service worker and push manager supported // Check whether service worker and push manager supported
const isSwSupported = const isSwSupported =
('serviceWorker' in navigator) && ('PushManager' in window); ('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 // A public key your push server will use to send
// messages to client apps via a push server. // messages to client apps via a push server.
applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey) applicationServerKey: urlBase64ToUint8Array(swPublickey)
}; };
// Subscribe push notification // Subscribe push notification

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,10 @@ html, body
text-size-adjust 100% text-size-adjust 100%
font-family sans-serif font-family sans-serif
html.changing-theme
&, *
transition background 1s ease !important
a a
text-decoration none text-decoration none
color var(--link) color var(--link)

View File

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

View File

@ -0,0 +1,61 @@
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));
});
let activeDeliverJobs = 0;
let activeInboxJobs = 0;
deliverQueue.on('global:active', () => {
activeDeliverJobs++;
});
inboxQueue.on('global:active', () => {
activeInboxJobs++;
});
async function tick() {
const deliverJobCounts = await deliverQueue.getJobCounts();
const inboxJobCounts = await inboxQueue.getJobCounts();
const stats = {
deliver: {
activeSincePrevTick: activeDeliverJobs,
active: deliverJobCounts.active,
waiting: deliverJobCounts.waiting,
delayed: deliverJobCounts.delayed
},
inbox: {
activeSincePrevTick: activeInboxJobs,
active: inboxJobCounts.active,
waiting: inboxJobCounts.waiting,
delayed: inboxJobCounts.delayed
}
};
ev.emit('queueStats', stats);
log.unshift(stats);
if (log.length > 200) log.pop();
activeDeliverJobs = 0;
activeInboxJobs = 0;
}
tick();
setInterval(tick, interval);
}

View File

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

View File

@ -16,6 +16,7 @@ import Xev from 'xev';
import Logger from './services/logger'; import Logger from './services/logger';
import serverStats from './daemons/server-stats'; import serverStats from './daemons/server-stats';
import notesStats from './daemons/notes-stats'; import notesStats from './daemons/notes-stats';
import queueStats from './daemons/queue-stats';
import loadConfig from './config/load'; import loadConfig from './config/load';
import { Config } from './config/types'; import { Config } from './config/types';
import { lessThan } from './prelude/array'; import { lessThan } from './prelude/array';
@ -50,6 +51,7 @@ function main() {
if (program.daemons) { if (program.daemons) {
serverStats(); serverStats();
notesStats(); notesStats();
queueStats();
} }
} }
@ -68,13 +70,16 @@ function greet() {
console.log(' |_|_|_|_|___|___|_,_|___|_ |'); console.log(' |_|_|_|_|___|___|_,_|___|_ |');
console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length))); console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length)));
//#endregion //#endregion
}
console.log(chalk`${os.hostname()} {gray (PID: ${process.pid.toString()})}`); console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
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()})} >`);
}
bootLogger.info('Welcome to Misskey!'); bootLogger.info('Welcome to Misskey!');
bootLogger.info(`Misskey v${pkg.version}`, null, true); bootLogger.info(`Misskey v${pkg.version}`, null, true);
bootLogger.info('Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
} }
/** /**
@ -114,9 +119,6 @@ async function masterMain() {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
// start queue
require('./queue').default();
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
} }
@ -127,6 +129,9 @@ async function workerMain() {
// start server // start server
await require('./server').default(); await require('./server').default();
// start job queue
require('./queue').default();
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send('ready'); process.send('ready');
@ -147,13 +152,9 @@ async function queueMain() {
bootLogger.succ('Misskey initialized'); bootLogger.succ('Misskey initialized');
// start processor // start processor
const queue = require('./queue').default(); require('./queue').default();
if (queue) { bootLogger.succ('Queue started', null, true);
bootLogger.succ('Queue started', null, true);
} else {
bootLogger.error('Queue not available');
}
} }
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));

View File

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

12
src/misc/check-svg.ts Normal file
View File

@ -0,0 +1,12 @@
import * as fs from 'fs';
import * as isSvg from 'is-svg';
export default function(path: string) {
try {
const size = fs.statSync(path).size;
if (size > 1 * 1024 * 1024) return false;
return isSvg(fs.readFileSync(path));
} catch {
return false;
}
}

View File

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

View File

@ -4,6 +4,7 @@ import AccessToken from './access-token';
import db from '../db/mongodb'; import db from '../db/mongodb';
import isObjectId from '../misc/is-objectid'; import isObjectId from '../misc/is-objectid';
import config from '../config'; import config from '../config';
import { dbLogger } from '../db/logger';
const App = db.get<IApp>('apps'); const App = db.get<IApp>('apps');
App.createIndex('secret'); 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 // Rename _id to id
_app.id = _app._id; _app.id = _app._id;
delete _app._id; delete _app._id;

View File

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

View File

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

View File

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

View File

@ -1,164 +1,187 @@
import * as Queue from 'bee-queue'; import * as Queue from 'bull';
import * as httpSignature from 'http-signature'; import * as httpSignature from 'http-signature';
import config from '../config'; import config from '../config';
import { ILocalUser } from '../models/user'; import { ILocalUser } from '../models/user';
import { program } from '../argv'; import { program } from '../argv';
import handler from './processors';
import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
import processDb from './processors/db';
import { queueLogger } from './logger'; import { queueLogger } from './logger';
import { IDriveFile } from '../models/drive-file';
const enableQueue = !program.disableQueue; function initializeQueue(name: string) {
const enableQueueProcessing = !program.onlyServer && enableQueue; return new Queue(name, config.redis != null ? {
const queueAvailable = config.redis != null; redis: {
port: config.redis.port,
const queue = initializeQueue(); host: config.redis.host,
password: config.redis.pass,
function initializeQueue() { db: config.redis.db || 0,
if (queueAvailable && enableQueue) { },
return new Queue('misskey-queue', { prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue'
redis: { } : null);
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;
}
} }
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.warn(`failed(${err}) id=${job.id} to=${job.data.to}`))
.on('error', (error) => deliverLogger.error(`error ${error}`))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
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.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`))
.on('error', (error) => inboxLogger.error(`error ${error}`))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
export function deliver(user: ILocalUser, content: any, to: any) { export function deliver(user: ILocalUser, content: any, to: any) {
if (content == null) return; if (content == null) return null;
const data = { const data = {
type: 'deliver',
user, user,
content, content,
to to
}; };
if (queueAvailable && enableQueueProcessing) { return deliverQueue.add(data, {
return queue.createJob(data) attempts: 8,
.retries(8) backoff: {
.backoff('exponential', 1000) type: 'exponential',
.save(); delay: 60 * 1000
} else { },
return handler({ data }, () => {}); removeOnComplete: true,
} removeOnFail: true
});
} }
export function processInbox(activity: any, signature: httpSignature.IParsedSignature) { export function inbox(activity: any, signature: httpSignature.IParsedSignature) {
const data = { const data = {
type: 'processInbox',
activity: activity, activity: activity,
signature signature
}; };
if (queueAvailable && enableQueueProcessing) { return inboxQueue.add(data, {
return queue.createJob(data) attempts: 8,
.retries(3) backoff: {
.backoff('exponential', 500) type: 'exponential',
.save(); delay: 1000
} else { },
return handler({ data }, () => {}); removeOnComplete: true,
} removeOnFail: true
});
} }
export function createDeleteNotesJob(user: ILocalUser) { export function createDeleteNotesJob(user: ILocalUser) {
const data = { return dbQueue.add('deleteNotes', {
type: 'deleteNotes',
user: user user: user
}; }, {
removeOnComplete: true,
if (queueAvailable && enableQueueProcessing) { removeOnFail: true
return queue.createJob(data).save(); });
} else {
return handler({ data }, () => {});
}
} }
export function createDeleteDriveFilesJob(user: ILocalUser) { export function createDeleteDriveFilesJob(user: ILocalUser) {
const data = { return dbQueue.add('deleteDriveFiles', {
type: 'deleteDriveFiles',
user: user user: user
}; }, {
removeOnComplete: true,
if (queueAvailable && enableQueueProcessing) { removeOnFail: true
return queue.createJob(data).save(); });
} else {
return handler({ data }, () => {});
}
} }
export function createExportNotesJob(user: ILocalUser) { export function createExportNotesJob(user: ILocalUser) {
const data = { return dbQueue.add('exportNotes', {
type: 'exportNotes',
user: user user: user
}; }, {
removeOnComplete: true,
if (queueAvailable && enableQueueProcessing) { removeOnFail: true
return queue.createJob(data).save(); });
} else {
return handler({ data }, () => {});
}
} }
export function createExportFollowingJob(user: ILocalUser) { export function createExportFollowingJob(user: ILocalUser) {
const data = { return dbQueue.add('exportFollowing', {
type: 'exportFollowing',
user: user user: user
}; }, {
removeOnComplete: true,
if (queueAvailable && enableQueueProcessing) { removeOnFail: true
return queue.createJob(data).save(); });
} else {
return handler({ data }, () => {});
}
} }
export function createExportMuteJob(user: ILocalUser) { export function createExportMuteJob(user: ILocalUser) {
const data = { return dbQueue.add('exportMute', {
type: 'exportMute',
user: user user: user
}; }, {
removeOnComplete: true,
if (queueAvailable && enableQueueProcessing) { removeOnFail: true
return queue.createJob(data).save(); });
} else {
return handler({ data }, () => {});
}
} }
export function createExportBlockingJob(user: ILocalUser) { export function createExportBlockingJob(user: ILocalUser) {
const data = { return dbQueue.add('exportBlocking', {
type: 'exportBlocking',
user: user user: user
}; }, {
removeOnComplete: true,
removeOnFail: true
});
}
if (queueAvailable && enableQueueProcessing) { export function createExportUserListsJob(user: ILocalUser) {
return queue.createJob(data).save(); return dbQueue.add('exportUserLists', {
} else { user: user
return handler({ data }, () => {}); }, {
} removeOnComplete: true,
removeOnFail: true
});
}
export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
return dbQueue.add('importFollowing', {
user: user,
fileId: fileId
}, {
removeOnComplete: true,
removeOnFail: true
});
}
export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
return dbQueue.add('importUserLists', {
user: user,
fileId: fileId
}, {
removeOnComplete: true,
removeOnFail: true
});
} }
export default function() { export default function() {
if (queueAvailable && enableQueueProcessing) { if (!program.onlyServer) {
queue.process(128, handler); deliverQueue.process(128, processDeliver);
queueLogger.succ('Processing started'); inboxQueue.process(128, processInbox);
processDb(dbQueue);
} }
return queue;
} }
export function destroy() { export function destroy() {
queue.destroy().then(n => { deliverQueue.once('cleaned', (jobs, status) => {
queueLogger.succ(`All job removed (${n} jobs)`); 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 * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import User from '../../models/user'; import User from '../../../models/user';
import DriveFile from '../../models/drive-file'; import DriveFile from '../../../models/drive-file';
import deleteFile from '../../services/drive/delete-file'; import deleteFile from '../../../services/drive/delete-file';
const logger = queueLogger.createSubLogger('delete-drive-files'); 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} ...`); logger.info(`Deleting drive files of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -32,7 +32,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
if (files.length === 0) { if (files.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -47,7 +47,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
userId: user._id, 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.`); 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 * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import Note from '../../models/note'; import Note from '../../../models/note';
import deleteNote from '../../services/note/delete'; import deleteNote from '../../../services/note/delete';
import User from '../../models/user'; import User from '../../../models/user';
const logger = queueLogger.createSubLogger('delete-notes'); 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} ...`); logger.info(`Deleting notes of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -32,7 +32,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
if (notes.length === 0) { if (notes.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -47,7 +47,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
userId: user._id, 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.`); 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 tmp from 'tmp';
import * as fs from 'fs'; import * as fs from 'fs';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import addFile from '../../services/drive/add-file'; import addFile from '../../../services/drive/add-file';
import User from '../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Blocking from '../../models/blocking'; import Blocking from '../../../models/blocking';
import config from '../../config'; import config from '../../../config';
const logger = queueLogger.createSubLogger('export-blocking'); 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} ...`); logger.info(`Exporting blocking of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -48,7 +48,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
if (blockings.length === 0) { if (blockings.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -74,7 +74,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
blockerId: user._id, blockerId: user._id,
}); });
if (job.reportProgress) job.reportProgress(exportedCount / total); job.progress(exportedCount / total);
} }
stream.end(); 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 tmp from 'tmp';
import * as fs from 'fs'; import * as fs from 'fs';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import addFile from '../../services/drive/add-file'; import addFile from '../../../services/drive/add-file';
import User from '../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Following from '../../models/following'; import Following from '../../../models/following';
import config from '../../config'; import config from '../../../config';
const logger = queueLogger.createSubLogger('export-following'); 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} ...`); logger.info(`Exporting following of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -48,7 +48,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
if (followings.length === 0) { if (followings.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -74,7 +74,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
followerId: user._id, followerId: user._id,
}); });
if (job.reportProgress) job.reportProgress(exportedCount / total); job.progress(exportedCount / total);
} }
stream.end(); 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 tmp from 'tmp';
import * as fs from 'fs'; import * as fs from 'fs';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import addFile from '../../services/drive/add-file'; import addFile from '../../../services/drive/add-file';
import User from '../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Mute from '../../models/mute'; import Mute from '../../../models/mute';
import config from '../../config'; import config from '../../../config';
const logger = queueLogger.createSubLogger('export-mute'); 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} ...`); logger.info(`Exporting mute of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -48,7 +48,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
if (mutes.length === 0) { if (mutes.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -74,7 +74,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
muterId: user._id, muterId: user._id,
}); });
if (job.reportProgress) job.reportProgress(exportedCount / total); job.progress(exportedCount / total);
} }
stream.end(); 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 tmp from 'tmp';
import * as fs from 'fs'; import * as fs from 'fs';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { queueLogger } from '../logger'; import { queueLogger } from '../../logger';
import Note, { INote } from '../../models/note'; import Note, { INote } from '../../../models/note';
import addFile from '../../services/drive/add-file'; import addFile from '../../../services/drive/add-file';
import User from '../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
const logger = queueLogger.createSubLogger('export-notes'); 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} ...`); logger.info(`Exporting notes of ${job.data.user._id} ...`);
const user = await User.findOne({ const user = await User.findOne({
@ -58,7 +58,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
if (notes.length === 0) { if (notes.length === 0) {
ended = true; ended = true;
if (job.reportProgress) job.reportProgress(100); job.progress(100);
break; break;
} }
@ -83,7 +83,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
userId: user._id, userId: user._id,
}); });
if (job.reportProgress) job.reportProgress(exportedNotesCount / total); job.progress(exportedNotesCount / total);
} }
await new Promise((res, rej) => { 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

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

View File

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

View File

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

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'; const logger = new Logger('deliver');
import { queueLogger } from '../../logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import instanceChart from '../../../services/chart/instance';
export default async (job: bq.Job, done: any): Promise<void> => { let latest: string = null;
export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to); const { host } = new URL(job.data.to);
try { 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); await request(job.data.user, job.data.to, job.data.content);
// Update stats // Update stats
@ -26,7 +33,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
instanceChart.requestSent(i.host, true); instanceChart.requestSent(i.host, true);
}); });
done(); return 'Success';
} catch (res) { } catch (res) {
// Update stats // Update stats
registerOrFetchInstanceDoc(host).then(i => { registerOrFetchInstanceDoc(host).then(i => {
@ -42,18 +49,21 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}); });
if (res != null && res.hasOwnProperty('statusCode')) { 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) { if (res.statusCode >= 400 && res.statusCode < 500) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく // 何回再送しても成功することはないということなのでエラーにはしないでおく
done(); return `${res.statusCode} ${res.statusMessage}`;
} else {
done(res.statusMessage);
} }
// 5xx etc.
throw `${res.statusCode} ${res.statusMessage}`;
} else { } else {
queueLogger.warn(`deliver failed: ${res} to=${job.data.to}`); // DNS error, socket error, timeout ...
done(); 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 * as httpSignature from 'http-signature';
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../misc/acct/parse';
import User, { IRemoteUser } from '../../../models/user'; import User, { IRemoteUser } from '../../models/user';
import perform from '../../../remote/activitypub/perform'; import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { URL } from 'url'; import { URL } from 'url';
import { publishApLogStream } from '../../../services/stream'; import { publishApLogStream } from '../../services/stream';
import Logger from '../../../services/logger'; import Logger from '../../services/logger';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance'; import Instance from '../../models/instance';
import instanceChart from '../../../services/chart/instance'; import instanceChart from '../../services/chart/instance';
const logger = new Logger('inbox'); const logger = new Logger('inbox');
// ユーザーの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 signature = job.data.signature;
const activity = job.data.activity; 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)); const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
if (host === null) { if (host === null) {
logger.warn(`request was made by local user: @${username}`); logger.warn(`request was made by local user: @${username}`);
done();
return; return;
} }
@ -42,7 +41,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
ValidateActivity(activity, host); ValidateActivity(activity, host);
} catch (e) { } catch (e) {
logger.warn(e.message); logger.warn(e.message);
done();
return; return;
} }
@ -50,8 +48,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: host.toLowerCase() }); const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) { if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`); logger.info(`Blocked request: ${host}`);
done();
return; return;
} }
@ -63,7 +60,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
ValidateActivity(activity, host); ValidateActivity(activity, host);
} catch (e) { } catch (e) {
logger.warn(e.message); logger.warn(e.message);
done();
return; return;
} }
@ -72,7 +68,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
const instance = await Instance.findOne({ host: host.toLowerCase() }); const instance = await Instance.findOne({ host: host.toLowerCase() });
if (instance && instance.isBlocked) { if (instance && instance.isBlocked) {
logger.warn(`Blocked request: ${host}`); logger.warn(`Blocked request: ${host}`);
done();
return; return;
} }
@ -82,7 +77,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}) as IRemoteUser; }) as IRemoteUser;
} }
// Update activityの場合は、ここで署名検証/更新処理まで実施して終了 // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') { if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') { if (activity.object && activity.object.type === 'Person') {
if (user == null) { if (user == null) {
@ -92,9 +87,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
} else { } else {
updatePerson(activity.actor, null, activity.object); updatePerson(activity.actor, null, activity.object);
} }
return;
} }
done();
return;
} }
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
@ -103,13 +97,11 @@ export default async (job: bq.Job, done: any): Promise<void> => {
} }
if (user === null) { if (user === null) {
done(new Error('failed to resolve user')); throw new Error('failed to resolve user');
return;
} }
if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
logger.error('signature verification failed'); logger.error('signature verification failed');
done();
return; return;
} }
@ -136,12 +128,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}); });
// アクティビティを処理 // アクティビティを処理
try { await perform(user, activity);
await perform(user, activity);
done();
} catch (e) {
done(e);
}
}; };
/** /**

View File

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

View File

@ -29,7 +29,19 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
return; 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}`); logger.info(`Creating the (Re)Note: ${uri}`);

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { Object } from '../type';
import { IRemoteUser } from '../../../models/user'; import { IRemoteUser } from '../../../models/user';
import create from './create'; import create from './create';
import performDeleteActivity from './delete'; import performDeleteActivity from './delete';
import performUpdateActivity from './update';
import follow from './follow'; import follow from './follow';
import undo from './undo'; import undo from './undo';
import like from './like'; import like from './like';
@ -23,6 +24,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await performDeleteActivity(actor, activity); await performDeleteActivity(actor, activity);
break; break;
case 'Update':
await performUpdateActivity(actor, activity);
break;
case 'Follow': case 'Follow':
await follow(actor, activity); await follow(actor, activity);
break; 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 instance = await fetchMeta();
const cache = instance.cacheRemoteFiles; 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) { if (file.metadata.isRemote) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、

View File

@ -18,6 +18,7 @@ import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote'; import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger'; import { apLogger } from '../logger';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
const logger = apLogger; 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> { export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
if (resolver == null) resolver = new Resolver(); 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') { if (!object || !['Note', 'Question'].includes(object.type)) {
logger.error(`invalid note: ${object}`); logger.error(`invalid note: ${value}`, {
resolver: {
history: resolver.getHistory()
},
value: value,
object: object
});
return null; return null;
} }
const note: INoteActivityStreamsObject = object; const note: INoteActivityStreamsObject = object;
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
logger.info(`Creating the Note: ${note.id}`); logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
@ -72,6 +81,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
} }
//#region Visibility //#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 visibility = 'public';
let visibleUsers: IUser[] = []; let visibleUsers: IUser[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { 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'; visibility = 'specified';
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
} }
} }
//#endergion //#endergion
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); 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は必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
// Noteがsensitiveなら添付もsensitiveにする // Noteがsensitiveなら添付もsensitiveにする
const limit = promiseLimit(2); const limit = promiseLimit(2);
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment const files = note.attachment
.map(attach => attach.sensitive = note.sensitive) .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; 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 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 // vote
if (reply && reply.poll && text != null) { if (reply && reply.poll) {
const m = text.match(/([0-9])$/); const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (m) { if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`); logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await vote(actor, reply, Number(m[1])); } 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; 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 apEmojis = emojis.map(emoji => emoji.name);
const questionUri = note._misskey_question; 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) { 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, { return await post(actor, {
createdAt: new Date(note.published), createdAt: new Date(note.published),
files: files, files,
reply, reply,
renote: quote, renote: quote,
cw: cw, cw,
text: text, text,
viaMobile: false, viaMobile: false,
localOnly: false, localOnly: false,
geo: undefined, 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 Resolver from '../resolver';
import { IQuestion } from '../type';
import { apLogger } from '../logger';
export async function extractPollFromQuestion(questionUri: string): Promise<IPoll> { export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
const resolver = new Resolver(); const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
const question = await resolver.resolve(questionUri) as any; const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : null;
const choices: IChoice[] = question.oneOf.map((x: any, i: number) => { if (multiple && !question.anyOf) {
return { throw 'invalid question';
id: i, }
text: x.name,
votes: x._misskey_votes || 0, const choices = question[multiple ? 'anyOf' : 'oneOf']
} as IChoice; .map((x, i) => ({
}); id: i,
text: x.name,
votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
} as IChoice));
return { 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([]); : Promise.resolve([]);
let inReplyTo; let inReplyTo;
let inReplyToNote: INote;
if (note.replyId) { if (note.replyId) {
const inReplyToNote = await Note.findOne({ inReplyToNote = await Note.findOne({
_id: note.replyId, _id: note.replyId,
}); });
@ -134,6 +135,29 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
...apemojis, ...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 { return {
id: `${config.url}/notes/${note._id}`, id: `${config.url}/notes/${note._id}`,
type: 'Note', type: 'Note',
@ -149,7 +173,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
inReplyTo, inReplyTo,
attachment: files.map(renderDocument), attachment: files.map(renderDocument),
sensitive: files.some(file => file.metadata.isSensitive), sensitive: files.some(file => file.metadata.isSensitive),
tag tag,
...asPoll
}; };
} }

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