Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
f5be8fd313 | |||
7f835d7f76 | |||
ddbb7c5993 | |||
00a3fe39e8 | |||
7537fb88d4 | |||
a81bc71a1e | |||
0a0aa0e2db | |||
c56b94ae96 | |||
e90712706d | |||
eb0623331f | |||
d15bd59109 | |||
60e0b19372 | |||
922eb937ff | |||
87573284f1 | |||
a91c585f55 | |||
953ea21d5e | |||
ecb00968bc | |||
50ad8adb2d | |||
16878caf09 | |||
5bc30c5493 | |||
85d89cf4c4 | |||
db693f598b | |||
0494c770a1 | |||
c473b62aed | |||
f19ac5320e | |||
612e3aafbc | |||
0e97fec451 | |||
e8c8626ee4 | |||
d89e0f07f8 | |||
e7f81a42ce | |||
ac614148b8 | |||
5eb02b4901 | |||
65631525f6 | |||
969435cfe9 | |||
c932f7a25b | |||
42d164dc57 | |||
a7e60f80bd | |||
3dd5f313b7 | |||
883962c393 | |||
8a30ff1c76 | |||
e47c354916 | |||
496f42805d | |||
c3d34bda37 | |||
bb6ede2b8f | |||
822400a1ba | |||
e3e08843f1 | |||
ce0d4f77fa | |||
94fdb4e974 | |||
4d425fc8a4 | |||
c6cdfa2f5a | |||
0fff2e4f16 | |||
80a2172715 | |||
5a0a297634 | |||
948a133b7b | |||
2ee826c958 | |||
539409faf8 | |||
606e46e4d7 | |||
a179cfd69a | |||
d8379253d4 | |||
c3344fbd68 | |||
4cebd6e84a | |||
90fbf9dbb0 | |||
d365b9f634 | |||
a2f06acaa4 | |||
8c90cbcbfb | |||
a4a47772dc | |||
5dde1f4602 | |||
9dc0909eeb | |||
0ed2592e41 | |||
76cff98220 | |||
60604b6f51 | |||
f410b7aecb | |||
1a61f2cee9 | |||
78a8293520 | |||
03cfb4fc8d | |||
144345a359 | |||
fd2c01515e | |||
219570e08b | |||
69df556ff5 | |||
5f4a52574f | |||
5a1f6c5839 | |||
91d0342fe8 | |||
8cc236daf8 | |||
d283ec69f7 | |||
d1aea7596c | |||
c934987b14 | |||
00c9f4a2e5 | |||
6605c1d07f | |||
7325d66c52 | |||
a485061e22 | |||
1f63f50343 | |||
cd3170dabd | |||
841cedc5f8 | |||
7f4882734d | |||
e7d647d412 | |||
913d14a58a | |||
909272ec3d | |||
7af40ffbbe | |||
9df79a3ec9 | |||
4f2eee06aa | |||
1b9cf76008 | |||
d035a43ed6 | |||
95ee9a6e09 | |||
02a63cdcb3 | |||
f02125dd47 | |||
c11e813146 | |||
a365849048 | |||
a493c9f769 | |||
a13f522b2a | |||
1ed70b2e2c | |||
86d5a599b7 | |||
c226fc8d63 | |||
bbf4e1c413 | |||
a24a20a83d | |||
725600da8f | |||
f74a32ed9b | |||
e08e72dd10 | |||
ce02e1e528 | |||
0b27d8a717 | |||
2782e7d26f | |||
2c83a05e80 | |||
467f68502a | |||
d95b0dee6b | |||
a1f3323fa5 | |||
494796a7f0 | |||
94f2c20d35 | |||
c1deb9438d | |||
ea86527c66 | |||
d1a18fe266 | |||
737064da82 |
14
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
14
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
@ -1,30 +1,30 @@
|
||||
---
|
||||
name: Bug Report
|
||||
name: 🐛 Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: ⚠️bug?
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the bug is -->
|
||||
|
||||
# Expected Behavior
|
||||
## Expected Behavior
|
||||
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
# Actual Behavior
|
||||
## Actual Behavior
|
||||
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
# Steps to Reproduce
|
||||
## Steps to Reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
# Environment
|
||||
## Environment
|
||||
|
||||
<!-- Tell us where on the platform it happens -->
|
||||
|
@ -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 -->
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
name: Feature Request
|
||||
name: ✨ Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
labels: ✨Feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
## Summary
|
||||
|
||||
<!-- Tell us what the suggestion is -->
|
@ -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 -->
|
@ -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 -->
|
@ -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 -->
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,4 +1,4 @@
|
||||
# Summary
|
||||
## Summary
|
||||
|
||||
<!--
|
||||
-
|
||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@ -1,6 +1,62 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
If you encounter any problems with updating, please try the following:
|
||||
1. `npm run clean` or `npm run cleanall`
|
||||
2. Retry update (Don't forget `npm i`)
|
||||
|
||||
10.93.1
|
||||
----------
|
||||
* データのエクスポートとインポートの動作を修正
|
||||
* デザインの調整
|
||||
|
||||
10.93.0
|
||||
----------
|
||||
* フォローリストをインポートできるように
|
||||
* embedプレイヤーを閉じれるように
|
||||
* リストをインポートしたときにプロキシアカウントがフォローするように修正
|
||||
* Web Share Targetの動作を修正
|
||||
* おすすめアンケートのチョイスを修正
|
||||
* デザインの調整
|
||||
|
||||
10.92.4
|
||||
----------
|
||||
* リストのエクスポートをできるように
|
||||
* ジョブキューウィジェットを追加
|
||||
* 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脆弱性があったので他のパッケージに差し替え
|
||||
|
12
README.md
12
README.md
@ -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>
|
||||
|
||||
[](https://misskey.xyz/)
|
||||
================================================================
|
||||
@ -103,7 +103,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td>
|
||||
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/3?token-time=2145916800&token-hash=c8HeVqLtmdgH-gSBJg8i10gmOcwllM87MDHeznl3el0%3D" alt="Melilot" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td>
|
||||
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=LtV2lRi3L2jOWMLwccr9qWYfPrFlzIo2jYZHKzHEb6k%3D" alt="Xeltica" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td>
|
||||
@ -124,6 +124,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1?token-time=2145916800&token-hash=95p8VdGX45E8BitZR_eOcDlqCjumjzNLBPQJrJdeCpI%3D" alt="takimura" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4?token-time=2145916800&token-hash=SbdZeN5SmsuT9stD6v0jN1z0hftg0FmRiCTxysU0Ihw%3D" alt="Damillora" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/935a10339daa4ede8e555903a0707060/1?token-time=2145916800&token-hash=3CrpqH-XtKs_NoIlSsTyVs8wCzP1WFCsG2xwps1IJq0%3D" alt="Atsuko Tominaga" width="100"></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
|
||||
@ -133,10 +134,12 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td>
|
||||
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
|
||||
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
|
||||
</tr></table>
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%3D" alt="natalie" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="Hekovic" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td>
|
||||
@ -144,13 +147,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
|
||||
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
|
||||
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
|
||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
||||
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||
</tr></table>
|
||||
|
||||
**Last updated:** Fri, 01 Mar 2019 23:59:07 UTC
|
||||
**Last updated:** Tue, 12 Mar 2019 00:50:06 UTC
|
||||
<!-- PATREON_END -->
|
||||
|
||||
:four_leaf_clover: Copyright
|
||||
|
4
assets/robots.txt
Normal file
4
assets/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
user-agent: *
|
||||
allow: /
|
||||
|
||||
# todo: sitemap
|
@ -118,7 +118,7 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex
|
||||
4. `NODE_ENV=production npm run build`
|
||||
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
|
||||
なにか問題が発生した場合は、`npm run clean`すると直る場合があります。
|
||||
なにか問題が発生した場合は、`npm run clean`または`npm run cleanall`すると直る場合があります。
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
import * as gulp from 'gulp';
|
||||
import * as gutil from 'gulp-util';
|
||||
import * as ts from 'gulp-typescript';
|
||||
const yaml = require('gulp-yaml');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
import tslint from 'gulp-tslint';
|
||||
const cssnano = require('gulp-cssnano');
|
||||
@ -126,12 +125,6 @@ gulp.task('copy:client', () =>
|
||||
.pipe(gulp.dest('./built/client/assets/'))
|
||||
);
|
||||
|
||||
gulp.task('locales', () =>
|
||||
gulp.src('./locales/*.yml')
|
||||
.pipe(yaml({ schema: 'DEFAULT_SAFE_SCHEMA' }))
|
||||
.pipe(gulp.dest('./built/client/assets/locales/'))
|
||||
);
|
||||
|
||||
gulp.task('doc', () =>
|
||||
gulp.src('./src/docs/**/*.styl')
|
||||
.pipe(stylus())
|
||||
@ -149,7 +142,6 @@ gulp.task('build', gulp.parallel(
|
||||
'build:ts',
|
||||
'build:copy',
|
||||
'build:client',
|
||||
'locales',
|
||||
'doc'
|
||||
));
|
||||
|
||||
|
@ -93,7 +93,7 @@ common:
|
||||
love: "Super"
|
||||
laugh: "Smích"
|
||||
hmm: "Hmm...?"
|
||||
surprise: "Páni"
|
||||
surprise: "Překvapení"
|
||||
congrats: "Gratuluji!"
|
||||
angry: "Naštvaný"
|
||||
confused: "Zmatený"
|
||||
@ -101,14 +101,14 @@ common:
|
||||
pudding: "Pudink"
|
||||
note-visibility:
|
||||
public: "Veřejná"
|
||||
home: "Domovský"
|
||||
home: "Domovská"
|
||||
home-desc: "Poslat pouze na domovskou časovou osu"
|
||||
followers: "Pro sledující"
|
||||
followers-desc: "Poslat pouze sledujícím"
|
||||
specified: "Přímý"
|
||||
specified: "Přímá"
|
||||
specified-desc: "Poslat pouze zmíněným uživatelům"
|
||||
local-public: "Veřejný (pouze místní)"
|
||||
local-home: "Domovský (pouze místní)"
|
||||
local-public: "Veřejná (pouze místní)"
|
||||
local-home: "Domovská (pouze místní)"
|
||||
local-followers: "Pro sledující (pouze místní)"
|
||||
note-placeholders:
|
||||
a: "Co právě děláte?"
|
||||
@ -122,7 +122,8 @@ common:
|
||||
profile: "Profil"
|
||||
notification: "Oznámení"
|
||||
apps: "Aplikace"
|
||||
tags: "Tagy"
|
||||
tags: "Hashtagy"
|
||||
mute-and-block: "Ztlumit/blokovat"
|
||||
blocking: "Blokování"
|
||||
security: "Zabezpečení"
|
||||
signin: "Historie přihlášení"
|
||||
@ -131,97 +132,183 @@ common:
|
||||
appearance: "Vzhled"
|
||||
behavior: "Chování"
|
||||
fetch-on-scroll: "Nekonečné rolování"
|
||||
note-visibility: "Viditelnost statusu"
|
||||
default-note-visibility: "Výchozí viditelnost statusu"
|
||||
fetch-on-scroll-desc: "Pokud budete rolovat dolů po stránce, automaticky bude načten další obsah."
|
||||
note-visibility: "Viditelnost příspěvku"
|
||||
default-note-visibility: "Výchozí viditelnost příspěvku"
|
||||
remember-note-visibility: "Zapamatovat viditelnost příspěvků"
|
||||
web-search-engine: "Webové vyhledávače"
|
||||
web-search-engine-desc: "Například: https://www.google.com/?#q={{query}}"
|
||||
keep-cw: "Zachovat varování o obsahu"
|
||||
keep-cw-desc: "Při odpovědi na příspěvek bude varování o obsahu nastaveno stejně jako původní příspěvek."
|
||||
i-like-sushi: "Mam radši sushi (než puding)"
|
||||
show-reversi-board-labels: "Zobrazit označení řad a sloupců v Reversi"
|
||||
use-avatar-reversi-stones: "Použít avatar jako figurku v Reversi"
|
||||
disable-animated-mfm: "Vypnout pohyblivé texty v příspěvku"
|
||||
disable-showing-animated-images: "Nepřehrávat animované obrázky"
|
||||
suggest-recent-hashtags: "Navrhovat nedávné hashtagy v rámci psacího pole"
|
||||
always-show-nsfw: "Vždycky ukázat NSFW obsah"
|
||||
always-mark-nsfw: "Označovat všechny příspěvky za delikátní"
|
||||
show-full-acct: "Zaradit hostovací server jako součast přezdívky"
|
||||
show-via: "zobrazit přes"
|
||||
reduce-motion: "Snížit pohyb v rozhraní"
|
||||
this-setting-is-this-device-only: "Pouze pro toto zařízení"
|
||||
use-os-default-emojis: "Použít výchozí emoji systému"
|
||||
line-width: "Hrubka línie"
|
||||
line-width-thin: "Úzka"
|
||||
line-width-normal: "Běžná"
|
||||
line-width-thick: "Tlustá"
|
||||
font-size: "Velikost písma"
|
||||
font-size-x-small: "Malé"
|
||||
font-size-small: "Dost malé"
|
||||
font-size-medium: "Průměrné"
|
||||
font-size-large: "Dost velké"
|
||||
font-size-x-large: "Velké"
|
||||
deck-column-align: "Zarovnání sloupců v Decku"
|
||||
deck-column-align-center: "Na střed"
|
||||
deck-column-align-left: "Vlevo"
|
||||
deck-column-width-wide: "široké"
|
||||
wallpaper: "Pozadí"
|
||||
deck-column-align-flexible: "Flexibilní"
|
||||
deck-column-width: "Šířka sloupců v Decku"
|
||||
deck-column-width-narrow: "Úzké"
|
||||
deck-column-width-narrower: "Poněkud úzké"
|
||||
deck-column-width-normal: "Normální"
|
||||
deck-column-width-wider: "Poněkud široké"
|
||||
deck-column-width-wide: "Široké"
|
||||
use-shadow: "Používat v rozhraní stíny"
|
||||
rounded-corners: "Zakulatit rohy v rozhraní"
|
||||
circle-icons: "Používat kulaté ikony"
|
||||
contrasted-acct: "Přidat uživatelskému účtu kontrast"
|
||||
wallpaper: "Obrázek na pozadí"
|
||||
choose-wallpaper: "Zvolit pozadí"
|
||||
delete-wallpaper: "Odstranit pozadí"
|
||||
post-form-on-timeline: "Zobrazit formulář pro nové příspěvky nad časovou osou"
|
||||
show-clock-on-header: "Zobrazit hodiny v pravém horním rohu"
|
||||
show-reply-target: "Zobrazit cíl odpovědi"
|
||||
timeline: "Časová osa"
|
||||
show-my-renotes: "Zobrazit moje renoty v časové ose"
|
||||
show-renoted-my-notes: "Zobrazit renoty vašich vlastních příspěvků v časové ose"
|
||||
show-local-renotes: "Zobrazit renoty místních příspěvků v časové ose"
|
||||
remain-deleted-note: "I nadále zobrazovat odstraněné příspěvky"
|
||||
sound: "Zvuk"
|
||||
enable-sounds: "Povolit zvuk"
|
||||
update: "Misskey aktualizace"
|
||||
enable-sounds-desc: "Přehrát zvuk, například při odeslání nebo přijetí příspěvku, či zprávy. Toto nastavení je uloženo v prohlížeči."
|
||||
volume: "Hlasitost"
|
||||
test: "Test"
|
||||
update: "Aktualizace Misskey"
|
||||
version: "Verze:"
|
||||
latest-version: "Nejnovější verze:"
|
||||
advanced-settings: "Pokročilé nastavení"
|
||||
navbar-position: "Pozice navigace"
|
||||
update-checking: "Kontroluji aktualizace"
|
||||
do-update: "Zkontrolovat aktualizace"
|
||||
update-settings: "Pokročilá nastavení"
|
||||
no-updates: "Nejsou dostupné žádné aktualizace"
|
||||
no-updates-desc: "Váš server Misskey je aktuální."
|
||||
update-available: "Je dostupná nová verze"
|
||||
update-available-desc: "Aktualizace budou aplikovány po znovunačtení stránky."
|
||||
advanced-settings: "Pokročilá nastavení"
|
||||
debug-mode: "Povolit režim ladění"
|
||||
debug-mode-desc: "Toto nastavení je uloženo v prohlížeči."
|
||||
navbar-position: "Poloha navigačního panelu"
|
||||
navbar-position-top: "Nahoře"
|
||||
navbar-position-left: "Vlevo"
|
||||
navbar-position-right: "Vpravo"
|
||||
i-am-under-limited-internet: "Mam omezený (pomalý) internet"
|
||||
post-style: "Styl zobrazení poznámek"
|
||||
post-style-standard: "Standardní"
|
||||
post-style-smart: "Chytrý"
|
||||
notification-position: "Poloha oznámení"
|
||||
notification-position-bottom: "Dole"
|
||||
notification-position-top: "Nahoře"
|
||||
load-raw-images: "Zobrazit obrázky v originální kvalitě"
|
||||
search: "Vyhledávání"
|
||||
disable-via-mobile: "Neoznačovat příspěvky jako „z mobilu“"
|
||||
load-raw-images: "Zobrazovat obrázky v původní kvalitě"
|
||||
load-remote-media: "Zobrazovat média ze vzdáleného serveru"
|
||||
search: "Hledání"
|
||||
delete: "Odstranit"
|
||||
loading: "Nahrávám..."
|
||||
loading: "Načítám..."
|
||||
ok: "OK"
|
||||
cancel: "Zrušit"
|
||||
update-available-title: "Aktualizace k dispozici"
|
||||
update-available: "Nová verze Misskey je k dispozici({newer}, Vaše verze je {current}). Načtěte znovu stránku pro aktivování nové verze."
|
||||
update-available: "Je k dispozici nová verze Misskey ({newer},vaše verze je {current}). Pro aplikování nové verze znovunačtěte stránku."
|
||||
verified-user: "Ověřené účty"
|
||||
hide-password: "Skrýt heslo"
|
||||
show-password: "Zobrazit heslo"
|
||||
do-not-use-in-production: "Tohle je vývojářský build. Nepoužívejte v produkci."
|
||||
user-suspended: "Tomuto uživateli byl pozastaven účet."
|
||||
is-remote-user: "Informace o tomto uživateli nemusí být kompletní."
|
||||
is-remote-post: "Obsah tohoto příspěvku je zrcadlen."
|
||||
view-on-remote: "Pro kompletnost jej zobrazte vzdáleně."
|
||||
renoted-by: "{user} renotoval/a"
|
||||
no-notes: "Bez poznámek"
|
||||
turn-on-darkmode: "Přepnout na tmavý režim"
|
||||
turn-off-darkmode: "Světlý režim"
|
||||
error:
|
||||
title: "Něco se stalo :("
|
||||
retry: "Zkusit znovu"
|
||||
reversi:
|
||||
drawn: "Remíza"
|
||||
my-turn: "Váš tah"
|
||||
opponent-turn: "Je řada na protivníkovi"
|
||||
turn-of: "{name} je na tahu"
|
||||
won: "{name} vyhrál"
|
||||
past-turn-of: "{name} byl/a na tahu"
|
||||
won: "{name} vyhrál/a"
|
||||
black: "Černá"
|
||||
white: "Bílá"
|
||||
total: "Celkem"
|
||||
this-turn: "{count}. kolo"
|
||||
widgets:
|
||||
analog-clock: "Analogové hodiny"
|
||||
profile: "Profil"
|
||||
calendar: "Kalendář"
|
||||
timemachine: "Kalendář (Stroj času)"
|
||||
activity: "Aktivita"
|
||||
rss: "RSS čtečka"
|
||||
memo: "Poznámky"
|
||||
memo: "Rychlé poznámky"
|
||||
trends: "Trendy"
|
||||
photo-stream: "Proud fotek"
|
||||
posts-monitor: "Grafy příspěvků"
|
||||
slideshow: "Prezentace"
|
||||
version: "Verze"
|
||||
broadcast: "Rozhlas"
|
||||
notifications: "Oznámení"
|
||||
users: "Doporučení uživatelé"
|
||||
polls: "Ankety"
|
||||
post-form: "Formulář pro psaní"
|
||||
server: "Informace o serveru"
|
||||
nav: "Navigace"
|
||||
tips: "Tipy"
|
||||
hashtags: "Hashtagy"
|
||||
dev: "Nepodařilo se vytvořit aplikace. Prosím zkuste to znovu."
|
||||
ai-chan-kawaii: "Ai-chan kawaii!"
|
||||
you: "Vy"
|
||||
auth/views/form.vue:
|
||||
share-access: "Chcete dovolit <i>{name}</i> přístup k Vašemu účtu?"
|
||||
share-access: "Chcete dovolit aplikaci <i>{name}</i> přístup k vašemu účtu?"
|
||||
permission-ask: "Tato aplikace vyžaduje následující oprávnění:"
|
||||
account-read: "Zobrazit informace účtu"
|
||||
note-write: "Odeslat."
|
||||
following-write: "Sledovat a přestat sledovat"
|
||||
drive-read: "Přečíst váš Disk"
|
||||
notification-read: "Sledovat oznámení."
|
||||
notification-write: "Zpravovat notifikace."
|
||||
cancel: "Zrušit"
|
||||
accept: "Povolit přístup"
|
||||
auth/views/index.vue:
|
||||
loading: "Nahrávám..."
|
||||
loading: "Načítám..."
|
||||
already-authorized: "Tato aplikace byla již autorizována."
|
||||
error: "Taková relace neexistuje."
|
||||
sign-in: "Prosím přihlaste se."
|
||||
common/views/pages/explore.vue:
|
||||
verified-users: "Ověřené účty"
|
||||
popular-users: "Populární uživatelé"
|
||||
recently-updated-users: "Nedávno aktívni uživatelé"
|
||||
recently-registered-users: "Nedávno registrovaní uživatelé"
|
||||
popular-tags: "Populární tagy"
|
||||
federated: "Z fediversu"
|
||||
federated: "Z fediverse"
|
||||
explore: "Prozkoumat {host}"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Otevřít v přehrávači"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "Žádní uživatelé"
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
matching:
|
||||
waiting-for: "Čeká se na {}"
|
||||
cancel: "Zrušit"
|
||||
common/views/components/games/reversi/reversi.game.vue:
|
||||
surrender: "Vzdát se"
|
||||
@ -235,14 +322,21 @@ common/views/components/games/reversi/reversi.index.vue:
|
||||
my-games: "Moje hra"
|
||||
all-games: "Všechny hry"
|
||||
enter-username: "Zadejte své uživatelské jméno"
|
||||
game-state:
|
||||
ended: "Ukončené"
|
||||
playing: "Probíhají"
|
||||
common/views/components/games/reversi/reversi.room.vue:
|
||||
settings-of-the-game: "Nastavení hry"
|
||||
choose-map: "Vybrat mapu"
|
||||
random: "Náhodně"
|
||||
black-or-white: "Černé/bílé"
|
||||
black-is: "Černá je {}"
|
||||
rules: "Pravidla"
|
||||
looped-map: "Zacyklená mapa"
|
||||
settings-of-the-bot: "Nastavení Botu"
|
||||
this-game-is-started-soon: "Hra začne za pár vteřin"
|
||||
waiting-for-other: "Čeká se na protivníka"
|
||||
waiting-for-me: "Čeká se na Vás"
|
||||
waiting-for-both: "Připravuji"
|
||||
cancel: "Zrušit"
|
||||
ready: "Připraveno"
|
||||
@ -255,7 +349,22 @@ common/views/components/connect-failed.troubleshooter.vue:
|
||||
checking-network: "Prověřit síťové připojení"
|
||||
internet: "Připojení k internetu"
|
||||
checking-internet: "Ověřuji připojení k internetu."
|
||||
server: "Připojení k serveru"
|
||||
no-network-desc: "Ujistěte se že jste připojeni k Internetu."
|
||||
no-internet: "Nejste připojeni k internetu"
|
||||
no-internet-desc: "Jste připojen k síti, ale zdá se že stále chybí připojení k Internetu. Prosím zkontrolujte Vaše připojení k Internetu."
|
||||
common/views/components/media-banner.vue:
|
||||
click-to-show: "Klikněte pro zobrazení"
|
||||
common/views/components/theme.vue:
|
||||
light-theme: "Šablona pro použití ve světlém vzhledu"
|
||||
dark-theme: "Šablona pro použití v tmavém vzhledu"
|
||||
light-themes: "Světlý vzhled"
|
||||
dark-themes: "Tmavý vzhled"
|
||||
install-a-theme: "Nainstalovat šablonu"
|
||||
theme-code: "Kód šablony"
|
||||
install: "Nainstalovat"
|
||||
installed: "\"{}\" byl nainstalován"
|
||||
create-a-theme: "Vytvořit motiv"
|
||||
base-theme: "Základní vzhled"
|
||||
find-more-theme: "Najít další vzhledy"
|
||||
theme-name: "Jméno vzhledu"
|
||||
@ -289,6 +398,7 @@ common/views/components/messaging-room.vue:
|
||||
only-one-file-attached: "Jenom JEDEN soubor může být přiložen ke zprávě."
|
||||
common/views/components/messaging-room.form.vue:
|
||||
send: "Odeslat"
|
||||
attach-from-local: "Přiložit soubory z Vašeho zařízení"
|
||||
only-one-file-attached: "Jenom JEDEN soubor může být přiložen ke zprávě."
|
||||
common/views/components/messaging-room.message.vue:
|
||||
is-read: "Přečtené"
|
||||
@ -301,13 +411,44 @@ common/views/components/nav.vue:
|
||||
donors: "Dárci"
|
||||
repository: "Úložiště"
|
||||
develop: "Vývojáři"
|
||||
feedback: "Zpětná vazba"
|
||||
common/views/components/note-menu.vue:
|
||||
mention: "Zmínění"
|
||||
detail: "Více"
|
||||
copy-content: "Zkopírovat obsah"
|
||||
copy-link: "Zkopírovat odkaz"
|
||||
favorite: "Přidat do oblíbených"
|
||||
unfavorite: "Odebrat z oblízených"
|
||||
watch: "Sledovat"
|
||||
unwatch: "Přestat sledovat"
|
||||
pin: "Připnout"
|
||||
unpin: "Odepnout"
|
||||
delete: "Odstranit"
|
||||
delete-confirm: "Opravdu chcete smazat tento příspěvek?"
|
||||
remote: "Ukázat originální poznámku"
|
||||
common/views/components/user-menu.vue:
|
||||
mention: "Zmínění"
|
||||
mute: "Umlčet"
|
||||
block: "Blokováno"
|
||||
unmute: "Zrušit umlčení"
|
||||
block: "Blokován"
|
||||
unblock: "Odblokovat"
|
||||
push-to-list: "Přidat do seznamu"
|
||||
select-list: "Vyberte seznam"
|
||||
report-abuse-reported: "Problém byl nahlášen administrátorovi. Děkujeme za Vaší kooperaci."
|
||||
common/views/components/poll.vue:
|
||||
vote-count: "{} hlasů"
|
||||
vote: "Hlasovat"
|
||||
show-result: "Podívat se na výsledky"
|
||||
voted: "Už jste hlasovaly"
|
||||
remaining-days: "zbývá {d} dnů, {h} hodin"
|
||||
remaining-hours: "zbývá {h} hodin, a {m} minut"
|
||||
remaining-minutes: "zbývá {m} minut, a {s} sekund"
|
||||
remaining-seconds: "zbývá {s} sekund"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "Musíte vybrat alespoň dvě možnosti"
|
||||
day: "Ne"
|
||||
common/views/components/emoji-picker.vue:
|
||||
custom-emoji: "Emoji"
|
||||
people: "Lidé"
|
||||
animals-and-nature: "Zvířata a příroda"
|
||||
food-and-drink: "Jídlo a pití"
|
||||
@ -360,20 +501,53 @@ common/views/components/notification-settings.vue:
|
||||
mark-as-read-all-notifications: "Označit všechna oznámení za přečtená"
|
||||
mark-as-read-all-unread-notes: "Označit všechny příspěvky za přečtené"
|
||||
mark-as-read-all-talk-messages: "Označit všechny zprávy za přečtené"
|
||||
common/views/components/integration-settings.vue:
|
||||
connect: "Připojit"
|
||||
disconnect: "Odpojit"
|
||||
common/views/components/github-setting.vue:
|
||||
description: "Jakmile spojíte Váš GitHub účet s Vaším Misskey účtem, uvidíte informace o Vašem GitHub účtu na Vašem profilu a budete se moci přihlásit skrze GitHub."
|
||||
connected-to: "Je připojen k tomuto GitHub účtu"
|
||||
detail: "Více…"
|
||||
reconnect: "Znovu připojit"
|
||||
connect: "Připojit Váš GitHub účet"
|
||||
disconnect: "Odpojit"
|
||||
common/views/components/discord-setting.vue:
|
||||
description: "Jakmile spojíte Váš Discord účet s Vaším Misskey účtem, uvidíte informace o Vašem Discord účtu na Vašem profilu a budete se moci přihlásit skrze Discord."
|
||||
connected-to: "Je připojen k tomuto Discord účtu"
|
||||
detail: "Více…"
|
||||
reconnect: "Znovu připojit"
|
||||
connect: "Připojit Váš Discord účet"
|
||||
disconnect: "Odpojit"
|
||||
common/views/components/uploader.vue:
|
||||
waiting: "Čekáme"
|
||||
common/views/components/visibility-chooser.vue:
|
||||
local-public: "Veřejný (pouze místní)"
|
||||
local-home: "Domovský (pouze místní)"
|
||||
public: "Veřejné"
|
||||
home: "Domů"
|
||||
specified-desc: "Poslat pouze zmíněným uživatelům"
|
||||
local-public: "Veřejná (pouze místní)"
|
||||
local-home: "Domovská (pouze místní)"
|
||||
local-followers: "Pro sledující (pouze místní)"
|
||||
common/views/components/trends.vue:
|
||||
count: "{} zmíněných uživatelů"
|
||||
empty: "Žádný trend"
|
||||
common/views/components/language-settings.vue:
|
||||
title: "Zobrazit jazyky"
|
||||
pick-language: "Zvolte jazyk"
|
||||
recommended: "Doporučené"
|
||||
info: "Pro aktivování změn musíte znovu načíst stránky."
|
||||
common/views/components/profile-editor.vue:
|
||||
title: "Profil"
|
||||
name: "Jméno"
|
||||
account: "Účet"
|
||||
location: "Lokace"
|
||||
description: "O mně"
|
||||
you-can-include-hashtags: "V popisku o Vás můžete použít i hastagy."
|
||||
language: "Jazyk"
|
||||
birthday: "Datum narození"
|
||||
avatar: "Avatar"
|
||||
banner: "Baner"
|
||||
is-cat: "Tento účet je kočka"
|
||||
is-bot: "Tento účet je Bot"
|
||||
advanced: "Ostatní"
|
||||
privacy: "Osobní údaje"
|
||||
save: "Uložit"
|
||||
@ -385,10 +559,12 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "Váš e-mail byl ověřen"
|
||||
email-not-verified: "Váš email není potvrzen. Prosím zkontrolujte si svou schránku."
|
||||
export: "Exportovat"
|
||||
import: "Importovat"
|
||||
export-targets:
|
||||
following-list: "Seznam následovníků"
|
||||
following-list: "Seznam sledujících"
|
||||
mute-list: "Seznam ztlumených uživatelů"
|
||||
blocking-list: "Seznam blokovaných uživatelů"
|
||||
user-lists: "Seznamy"
|
||||
enter-password: "Prosím, zadejte Vaše heslo"
|
||||
danger-zone: "Nebezpečná zóna"
|
||||
delete-account: "Smazat účet"
|
||||
@ -401,6 +577,7 @@ common/views/components/user-list-editor.vue:
|
||||
delete-are-you-sure: "Smazat seznam \"$1\"?"
|
||||
deleted: "Smazáno"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Načítám"
|
||||
next: "Další"
|
||||
common/views/widgets/calendar.vue:
|
||||
year: "Rok {}"
|
||||
@ -413,10 +590,12 @@ common/views/widgets/photo-stream.vue:
|
||||
no-photos: "Žádné obrázky"
|
||||
common/views/widgets/posts-monitor.vue:
|
||||
title: "Grafy příspěvků"
|
||||
toggle: "Přepnout zobrazení"
|
||||
common/views/widgets/hashtags.vue:
|
||||
title: "Hashtagy"
|
||||
common/views/widgets/server.vue:
|
||||
title: "Informace o serveru"
|
||||
toggle: "Přepnout zobrazení"
|
||||
common/views/widgets/memo.vue:
|
||||
title: "Poznámky"
|
||||
memo: "Pište sem!"
|
||||
@ -425,12 +604,19 @@ common/views/widgets/slideshow.vue:
|
||||
no-image: "V této složce nebyly nalezeny žádné fotky."
|
||||
desktop:
|
||||
banner: "Baner"
|
||||
avatar-crop-title: "Vyberte část, která se zobrazí jako avatar"
|
||||
avatar: "Avatar"
|
||||
uploading-avatar: "Nahrál nový avatar"
|
||||
avatar-updated: "Vaše avatar byl aktualizován"
|
||||
invalid-filetype: "Tento formát souboru není podporován"
|
||||
desktop/views/components/activity.chart.vue:
|
||||
total: "Černá ... Celkem"
|
||||
notes: "Modrá ... Poznámky"
|
||||
replies: "Červená ... Odpovědi"
|
||||
renotes: "Zelená ... Renoty"
|
||||
desktop/views/components/activity.vue:
|
||||
title: "Aktivita"
|
||||
toggle: "Přepnout zobrazení"
|
||||
desktop/views/components/calendar.vue:
|
||||
title: "{month}. {year}"
|
||||
prev: "Předchozí měsíc"
|
||||
@ -448,6 +634,8 @@ desktop/views/components/choose-folder-from-drive-window.vue:
|
||||
desktop/views/components/crop-window.vue:
|
||||
cancel: "Zrušit"
|
||||
ok: "OK"
|
||||
desktop/views/components/drive-window.vue:
|
||||
used: "využito"
|
||||
desktop/views/components/drive.file.vue:
|
||||
avatar: "Avatar"
|
||||
banner: "Baner"
|
||||
@ -479,10 +667,42 @@ desktop/views/components/drive.vue:
|
||||
empty-folder: "Tato složka je prázdná"
|
||||
unable-to-process: "Operace nemohla být dokončena."
|
||||
unhandled-error: "Neznámá chyba"
|
||||
url-upload: "Nahrát z URL adresy"
|
||||
url-of-file: "URL adresa souboru, který chcete nahrát"
|
||||
may-take-time: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
|
||||
create-folder: "Vytvořit složku"
|
||||
folder-name: "Název složky"
|
||||
contextmenu:
|
||||
create-folder: "Vytvořit složku"
|
||||
upload: "Nahrát soubor"
|
||||
url-upload: "Nahrát z URL"
|
||||
desktop/views/components/media-video.vue:
|
||||
click-to-show: "Klikněte pro zobrazení"
|
||||
desktop/views/components/game-window.vue:
|
||||
game: "Reversi"
|
||||
desktop/views/components/home.vue:
|
||||
done: "Hotovo"
|
||||
add: "Přidat"
|
||||
desktop/views/input-dialog.vue:
|
||||
cancel: "Zrušit"
|
||||
ok: "OK"
|
||||
desktop/views/components/messaging-room-window.vue:
|
||||
title: "Zprávy:"
|
||||
desktop/views/components/messaging-window.vue:
|
||||
title: "Zprávy"
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
renote: "Renotovat"
|
||||
add-reaction: "Přidat reakci"
|
||||
undo-reaction: "Odebrat reakci"
|
||||
desktop/views/components/note.vue:
|
||||
reply: "Odpovědět"
|
||||
renote: "Renote"
|
||||
add-reaction: "Přidat reakci"
|
||||
undo-reaction: "Odebrat reakci"
|
||||
detail: "Více"
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
desktop/views/components/notes.vue:
|
||||
error: "Načítání selhalo."
|
||||
@ -493,37 +713,62 @@ desktop/views/components/post-form.vue:
|
||||
hide-contents: "Schovat obsah"
|
||||
reply-placeholder: "Odpovědět na tento příspěvěk"
|
||||
quote-placeholder: "Citovat tento příspěvek"
|
||||
submit: "Příspěvek"
|
||||
reply: "Odpovědět"
|
||||
renote: "Renotovat"
|
||||
posted: "Odesláno!"
|
||||
replied: "Odpověděno!"
|
||||
reposted: "Renotováno!"
|
||||
note-failed: "Nepodařilo se přidat příspěvek"
|
||||
renote-failed: "Renotování neuspělo"
|
||||
insert-a-kao: "v('ω')v"
|
||||
create-poll: "Vytvořit anketu"
|
||||
text-remain: "{0} znaků zbývá"
|
||||
recent-tags: "Nejnovější"
|
||||
visibility: "Viditelnost"
|
||||
geolocation-alert: "Vaše zařízení nepodporuje lokační službu"
|
||||
error: "Chyba"
|
||||
enter-username: "Zadejte své uživatelské jméno..."
|
||||
desktop/views/components/post-form-window.vue:
|
||||
note: "Nový příspěvek"
|
||||
reply: "Odpovědět"
|
||||
desktop/views/components/progress-dialog.vue:
|
||||
waiting: "Čekáme"
|
||||
desktop/views/components/renote-form.vue:
|
||||
quote: "Citovat..."
|
||||
cancel: "Zrušit"
|
||||
renote: "Renotovat"
|
||||
renote-home: "Renote (domů)"
|
||||
reposting: "Renotuji..."
|
||||
success: "Renotováno!"
|
||||
failure: "Renotování neuspělo"
|
||||
desktop/views/components/renote-form-window.vue:
|
||||
title: "Chcete tohle renotovat?"
|
||||
desktop/views/components/settings.2fa.vue:
|
||||
detail: "Více…"
|
||||
url: "https://www.google.cz/landing/2step/"
|
||||
common/views/components/media-image.vue:
|
||||
click-to-show: "Klikněte pro zobrazení"
|
||||
common/views/components/api-settings.vue:
|
||||
token: "Token:"
|
||||
enter-password: "Prosím zadejte heslo"
|
||||
console:
|
||||
title: "API konzole"
|
||||
endpoint: "Endpoint"
|
||||
parameter: "Parametry"
|
||||
send: "Odeslat"
|
||||
sending: "Odesílám"
|
||||
response: "Výsledek"
|
||||
desktop/views/components/settings.apps.vue:
|
||||
no-apps: "Žádné připojené aplikace"
|
||||
common/views/components/drive-settings.vue:
|
||||
max: "Velikost úložiště"
|
||||
in-use: "využito"
|
||||
stats: "Statistiky"
|
||||
common/views/components/mute-and-block.vue:
|
||||
mute-and-block: "Umlčet / Blokovat"
|
||||
mute-and-block: "Umlčet/blokovat"
|
||||
mute: "Umlčet"
|
||||
block: "Blokováno"
|
||||
block: "Blokován"
|
||||
no-muted-users: "Žádný uživatel nebyl umlčen"
|
||||
no-blocked-users: "Žádný uživatel není blokován"
|
||||
save: "Uložit"
|
||||
@ -546,21 +791,55 @@ desktop/views/components/settings.tags.vue:
|
||||
desktop/views/components/taskmanager.vue:
|
||||
title: "Správce úloh"
|
||||
desktop/views/components/timeline.vue:
|
||||
home: "Domů"
|
||||
local: "Lokální"
|
||||
global: "Globální"
|
||||
mentions: "Zmínění"
|
||||
messages: "Zprávy"
|
||||
list: "Seznamy"
|
||||
hashtag: "Hashtag"
|
||||
add-list: "Přidat do seznamu"
|
||||
list-name: "Název seznamu"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Vítejte zpátky,"
|
||||
adjective: "Pán"
|
||||
desktop/views/components/ui.header.account.vue:
|
||||
profile: "Váš profil"
|
||||
lists: "Seznamy"
|
||||
admin: "Administrace"
|
||||
desktop/views/components/ui.header.nav.vue:
|
||||
game: "Hry"
|
||||
desktop/views/components/ui.header.notifications.vue:
|
||||
title: "Oznámení"
|
||||
desktop/views/components/ui.header.post.vue:
|
||||
post: "Nový příspěvek"
|
||||
desktop/views/components/ui.header.search.vue:
|
||||
placeholder: "Vyhledávání"
|
||||
desktop/views/components/received-follow-requests-window.vue:
|
||||
accept: "Přijmout"
|
||||
reject: "Odmítnout"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Seznamy uživatelů"
|
||||
create-list: "Vytvořit seznam"
|
||||
list-name: "Název seznamu"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Příspěvky"
|
||||
desktop/views/components/users-list.vue:
|
||||
all: "Všechny"
|
||||
iknow: "Znáte"
|
||||
fetching: "Načítám…"
|
||||
desktop/views/components/window.vue:
|
||||
close: "Zavřít"
|
||||
admin/views/index.vue:
|
||||
instance: "Instance"
|
||||
emoji: "Emoji"
|
||||
moderators: "Moderátoři"
|
||||
users: "Uživatelé"
|
||||
federation: "Z fediversu"
|
||||
announcements: "Oznámení"
|
||||
hashtags: "Hashtagy"
|
||||
queue: "Fronta úloh"
|
||||
logs: "Logy"
|
||||
back-to-misskey: "Zpět na Misskey"
|
||||
admin/views/dashboard.vue:
|
||||
accounts: "Účty"
|
||||
@ -611,9 +890,19 @@ admin/views/instance.vue:
|
||||
saved: "Uloženo"
|
||||
user-recommendation-config: "Doporučení uživatelé"
|
||||
email: "Emailová adresa"
|
||||
smtp-port: "SMTP Port"
|
||||
smtp-auth: "Provést SMTP autentikaci"
|
||||
smtp-user: "SMTP uživatel"
|
||||
smtp-pass: "SMTP heslo"
|
||||
serviceworker-config: "ServiceWorker"
|
||||
enable-serviceworker: "Povolit ServiceWorker"
|
||||
vapid-publickey: "VAPID veřejný klíč"
|
||||
vapid-privatekey: "VAPID osobní klíč"
|
||||
admin/views/charts.vue:
|
||||
title: "Graf"
|
||||
per-day: "za den"
|
||||
per-hour: "za hodinu"
|
||||
federation: "Federace"
|
||||
notes: "Příspěvky"
|
||||
users: "Uživatelé"
|
||||
drive: "Disk"
|
||||
@ -621,11 +910,20 @@ admin/views/charts.vue:
|
||||
charts:
|
||||
federation-instances: "Počet instancí: zvýšení/snížení"
|
||||
federation-instances-total: "Celkový počet instancí"
|
||||
notes-total: "Celkem příspěvků"
|
||||
users-total: "Celkem uživatelů"
|
||||
active-users: "Aktivní uživatelé"
|
||||
network-requests: "Požadavek"
|
||||
network-time: "Doba odezvy"
|
||||
network-usage: "Síťový provoz"
|
||||
admin/views/drive.vue:
|
||||
operation: "Operace"
|
||||
fileid-or-url: "ID nebo URL souboru"
|
||||
file-not-found: "Soubor nebyl nalezen"
|
||||
sort:
|
||||
title: "Seřadit"
|
||||
createdAtAsc: "Věk - od nejstaršího"
|
||||
createdAtDesc: "Věk - od nejmladšího"
|
||||
sizeAsc: "Velikost - od nejmenších"
|
||||
sizeDesc: "Velikost – od největších"
|
||||
origin:
|
||||
@ -642,8 +940,17 @@ admin/views/users.vue:
|
||||
reset-password: "Resetovat heslo"
|
||||
reset-password-confirm: "Opravdu chcete resetovat Vaše heslo?"
|
||||
password-updated: "Heslo je nyní \"{password}\""
|
||||
verify: "Ověřit účet"
|
||||
verify-confirm: "Chcete aby toto byl ověřený účet?"
|
||||
verified: "Účet se nyní ověřuje"
|
||||
unverify: "Zrušit ověření účtu"
|
||||
unverify-confirm: "Opravdu chcete zrušit designaci \"ověřený účet\"?"
|
||||
unverified: "Ruší se potvrzení účtu"
|
||||
update-remote-user: "Aktualizovat informace o vzdáleném účtu"
|
||||
users:
|
||||
title: "Uživatel"
|
||||
state:
|
||||
all: "Všechny"
|
||||
moderator: "Moderátor"
|
||||
adminOrModerator: "Admin/Moderátor"
|
||||
verified: "Ověřený účet"
|
||||
@ -695,61 +1002,180 @@ admin/views/federation.vue:
|
||||
users: "Uživatelé"
|
||||
status: "Status"
|
||||
latest-request-received-at: "Poslední požadavek přijat"
|
||||
block: "Blokováno"
|
||||
block: "Blokován"
|
||||
instances: "Instance"
|
||||
states:
|
||||
blocked: "Blokováno"
|
||||
all: "Všechny"
|
||||
blocked: "Blokován"
|
||||
not-responding: "Bez odpovědi"
|
||||
marked-as-closed: "Označeno jako uzavřené"
|
||||
charts: "Graf"
|
||||
chart-srcs:
|
||||
requests: "Požadavek"
|
||||
users-total: "Celkem uživatelů"
|
||||
notes-total: "Celkem příspěvků"
|
||||
chart-spans:
|
||||
hour: "za hodinu"
|
||||
day: "za den"
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "O Misskey"
|
||||
timeline: "Časová osa"
|
||||
announcements: "Oznámení"
|
||||
photos: "Nedávné obrázky"
|
||||
powered-by-misskey: "Běží na <b>Misskey</b>."
|
||||
info: "Informace"
|
||||
desktop/views/pages/drive.vue:
|
||||
title: "Misskey Disk"
|
||||
desktop/views/pages/note.vue:
|
||||
prev: "Předchozí příspěvěk"
|
||||
next: "Následující příspěvek"
|
||||
desktop/views/pages/selectdrive.vue:
|
||||
title: "Vyberte soubor(y)"
|
||||
ok: "OK"
|
||||
cancel: "Zrušit"
|
||||
upload: "Nahrajte soubory z vašeho zařízení"
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "Vyhledávání je vypnuté pro tuto instanci."
|
||||
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
users: "Uživatel"
|
||||
add-user: "Přidat uživatele"
|
||||
username: "Přezdívka"
|
||||
desktop/views/pages/user/user.followers-you-know.vue:
|
||||
loading: "Nahrávám..."
|
||||
loading: "Načítám..."
|
||||
desktop/views/pages/user/user.friends.vue:
|
||||
loading: "Nahrávám..."
|
||||
title: "Častá zmínění"
|
||||
loading: "Načítám..."
|
||||
no-users: "Žádná častá zmínění"
|
||||
desktop/views/pages/user/user.photos.vue:
|
||||
loading: "Nahrávám..."
|
||||
title: "Fotky"
|
||||
loading: "Načítám..."
|
||||
no-photos: "Žádné obrázky"
|
||||
desktop/views/pages/user/user.header.vue:
|
||||
posts: "Poznámky"
|
||||
month: "Po"
|
||||
day: "Ne"
|
||||
desktop/views/widgets/messaging.vue:
|
||||
title: "Zprávy"
|
||||
desktop/views/widgets/notifications.vue:
|
||||
title: "Oznámení"
|
||||
desktop/views/widgets/polls.vue:
|
||||
title: "Ankety"
|
||||
desktop/views/widgets/users.vue:
|
||||
title: "Doporučení uživatelé"
|
||||
mobile/views/components/drive.vue:
|
||||
used: "využito"
|
||||
file-count: "Soubor(ů)"
|
||||
folder-is-empty: "Tato složka je prázdná"
|
||||
deletion-alert: "Omlouváme se, ale mazání složek ještě nebylo implementováno."
|
||||
folder-name: "Název složky"
|
||||
url-prompt: "URL adresa souboru, který chcete nahrát"
|
||||
uploading: "Byl zahájen upload. Může chvilku trvat než bude dokončen."
|
||||
mobile/views/components/drive-file-chooser.vue:
|
||||
select-file: "Vybrat soubory"
|
||||
mobile/views/components/drive-folder-chooser.vue:
|
||||
select-folder: "Vyberte složku"
|
||||
mobile/views/components/drive.file-detail.vue:
|
||||
download: "Stáhnout"
|
||||
rename: "Přejmenovat"
|
||||
move: "Přesunout"
|
||||
hash: "Hash (md5)"
|
||||
exif: "EXIF"
|
||||
mobile/views/components/media-video.vue:
|
||||
click-to-show: "Klikněte pro zobrazení"
|
||||
common/views/components/follow-button.vue:
|
||||
follow-processing: "Zpracovávám"
|
||||
mobile/views/components/note.vue:
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
location: "Lokace"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Odpovědět"
|
||||
reaction: "Reakce"
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
location: "Lokace"
|
||||
mobile/views/components/note-preview.vue:
|
||||
admin: "admin"
|
||||
bot: "bot"
|
||||
cat: "kočka"
|
||||
mobile/views/components/note-sub.vue:
|
||||
admin: "admin"
|
||||
bot: "bot"
|
||||
cat: "kočka"
|
||||
mobile/views/components/post-form.vue:
|
||||
add-visible-user: "Přidat uživatele"
|
||||
submit: "Příspěvek"
|
||||
reply: "Odpovědět"
|
||||
renote: "Renotovat"
|
||||
reply-placeholder: "Odpovědět na tento příspěvěk"
|
||||
location-alert: "Vaše zařízení nepodporuje lokační službu"
|
||||
error: "Chyba"
|
||||
username-prompt: "Zadejte uživatelské jméno"
|
||||
mobile/views/components/sub-note-content.vue:
|
||||
private: "Tento příspěvek je soukromý"
|
||||
deleted: "Tento příspěvek byl odstraněn"
|
||||
poll: "Ankety"
|
||||
mobile/views/components/ui.header.vue:
|
||||
welcome-back: "Vítejte zpátky,"
|
||||
adjective: "Pán"
|
||||
mobile/views/components/ui.nav.vue:
|
||||
timeline: "Časová osa"
|
||||
notifications: "Oznámení"
|
||||
search: "Vyhledávání"
|
||||
user-lists: "Seznamy"
|
||||
widgets: "Widgety"
|
||||
game: "Hry"
|
||||
admin: "Administrace"
|
||||
about: "O Misskey"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Seznamy"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Váš účet je připraven! 📦"
|
||||
mobile/views/pages/home.vue:
|
||||
home: "Domů"
|
||||
local: "Lokální"
|
||||
global: "Globální"
|
||||
mentions: "Zmínění"
|
||||
messages: "Zprávy"
|
||||
mobile/views/pages/tag.vue:
|
||||
no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
|
||||
mobile/views/pages/widgets.vue:
|
||||
add-widget: "Přidat"
|
||||
customization-tips: "Tipy pro přizpůsobení"
|
||||
mobile/views/pages/widgets/activity.vue:
|
||||
activity: "Aktivita"
|
||||
mobile/views/pages/share.vue:
|
||||
share-with: "Sdílet na {name}"
|
||||
mobile/views/pages/received-follow-requests.vue:
|
||||
accept: "Přijmout"
|
||||
reject: "Odmítnout"
|
||||
mobile/views/pages/note.vue:
|
||||
prev: "Předchozí příspěvěk"
|
||||
next: "Následující příspěvek"
|
||||
mobile/views/pages/games/reversi.vue:
|
||||
reversi: "Reversi"
|
||||
mobile/views/pages/search.vue:
|
||||
not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
|
||||
mobile/views/pages/selectdrive.vue:
|
||||
select-file: "Vybrat soubory"
|
||||
mobile/views/pages/user/home.vue:
|
||||
activity: "Aktivita"
|
||||
frequently-replied-users: "Častá zmínění"
|
||||
mobile/views/pages/user/home.photos.vue:
|
||||
no-photos: "Žádné obrázky"
|
||||
deck:
|
||||
widgets: "Widgety"
|
||||
home: "Domů"
|
||||
local: "Lokální"
|
||||
hashtag: "Hashtagy"
|
||||
global: "Globální"
|
||||
mentions: "Zmínění"
|
||||
notifications: "Oznámení"
|
||||
list: "Seznamy"
|
||||
select-list: "Vyberte seznam"
|
||||
swap-left: "Posunout doleva"
|
||||
swap-right: "Posunout doprava"
|
||||
rename: "Přejmenovat"
|
||||
@ -758,4 +1184,10 @@ deck/deck.user-column.vue:
|
||||
dev/views/new-app.vue:
|
||||
app-name-desc: "Jméno vaší aplikace"
|
||||
app-desc: "Stručný popis nebo představení vaší aplikace."
|
||||
account-read: "Zobrazit informace účtu"
|
||||
note-write: "Odeslat."
|
||||
reaction-write: "Přidat nebo odebrat reakce."
|
||||
following-write: "Sledovat a přestat sledovat"
|
||||
drive-read: "Přečíst váš Disk"
|
||||
notification-read: "Sledovat oznámení."
|
||||
notification-write: "Zpravovat notifikace."
|
||||
|
@ -270,7 +270,6 @@ common/views/components/note-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Stimme für '{}'"
|
||||
vote-count: "{} Stimmen"
|
||||
total-users: "{} Nutzer haben abgestimmt"
|
||||
vote: "Abstimmen"
|
||||
show-result: "Zeige Ergebnis"
|
||||
voted: "Abgestimmt"
|
||||
@ -280,6 +279,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "Diese Auswahl entfernen"
|
||||
add: "+ Eine Auswahl hinzufügen"
|
||||
destroy: "Diese Abstimmung löschen"
|
||||
day: "So"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Wähle eine Reaktion aus"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -339,6 +339,9 @@ common/views/components/profile-editor.vue:
|
||||
banner: "Banner"
|
||||
save: "Speichern"
|
||||
export: "Exportieren"
|
||||
import: "Importieren"
|
||||
export-targets:
|
||||
user-lists: "Listen"
|
||||
enter-password: "Bitte Passwort eingeben"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Laden"
|
||||
|
@ -24,8 +24,8 @@ common:
|
||||
application-authorization: "Application authorizations"
|
||||
close: "Close"
|
||||
do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
|
||||
load-more: "Load more"
|
||||
enter-password: "Please enter the Password"
|
||||
load-more: "Read more"
|
||||
enter-password: "Enter your password"
|
||||
2fa: "Two-factor authentication"
|
||||
customize-home: "Customize home layout"
|
||||
featured-notes: "Featured notes"
|
||||
@ -114,7 +114,7 @@ common:
|
||||
a: "What are you doing?"
|
||||
b: "What's happening?"
|
||||
c: "What’s on your mind?"
|
||||
d: "Would you post any words?"
|
||||
d: "What do you want to say?"
|
||||
e: "Write here"
|
||||
f: "Waiting for your writing."
|
||||
settings: "Settings"
|
||||
@ -223,8 +223,8 @@ common:
|
||||
search: "Search"
|
||||
delete: "Delete"
|
||||
loading: "Loading"
|
||||
ok: "It's OK"
|
||||
cancel: "Quit"
|
||||
ok: "Confirm"
|
||||
cancel: "Exit"
|
||||
update-available-title: "Update available"
|
||||
update-available: "A new version of Misskey is now available({newer}, the current version is {current}). Reload the page to apply updates."
|
||||
my-token-regenerated: "Your token has been regenerated, so you will be signed out."
|
||||
@ -285,7 +285,7 @@ auth/views/form.vue:
|
||||
account-read: "View account information."
|
||||
account-write: "Modify account information."
|
||||
note-write: "Post."
|
||||
like-write: "React to posts."
|
||||
like-write: "Express yourself about this post."
|
||||
following-write: "Follow and unfollow."
|
||||
drive-read: "Read your drive."
|
||||
drive-write: "Upload/delete files in your drive."
|
||||
@ -304,7 +304,7 @@ auth/views/index.vue:
|
||||
error: "Session does not exist."
|
||||
sign-in: "Please sign in."
|
||||
common/views/pages/explore.vue:
|
||||
verified-users: "Verified accounts"
|
||||
verified-users: "Official accounts"
|
||||
popular-users: "Popular users"
|
||||
recently-updated-users: "Recently active users"
|
||||
recently-registered-users: "Users who joined recently"
|
||||
@ -314,6 +314,7 @@ common/views/pages/explore.vue:
|
||||
users-info: "Currently, {users} users are registered here"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Enable playback"
|
||||
disable-player: "Close the player"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "There are no users."
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
@ -489,16 +490,35 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Vote for '{}'"
|
||||
vote-count: "{} votes"
|
||||
total-users: "{} users voted"
|
||||
total-votes: "{} votes in total"
|
||||
vote: "Vote"
|
||||
show-result: "Show results"
|
||||
voted: "Voted"
|
||||
closed: "Ended"
|
||||
remaining-days: "{d} days, {h} hours remain"
|
||||
remaining-hours: "{h} hours, and {m} minutes remain"
|
||||
remaining-minutes: "{m} minutes, and {s} seconds remaining"
|
||||
remaining-seconds: "{s} seconds remaining"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "At least two choices are required"
|
||||
choice-n: "Choice {}"
|
||||
remove: "Delete the choice"
|
||||
add: "+ Add a choice"
|
||||
destroy: "Discard the poll"
|
||||
multiple: "More than one answer is allowed"
|
||||
expiration: "Valid until"
|
||||
infinite: "Indefinitely"
|
||||
at: "Date and time pick"
|
||||
after: "Progression specifics"
|
||||
no-more: "You cannot add any more"
|
||||
deadline-date: "Finish date"
|
||||
deadline-time: "Time duration"
|
||||
interval: "Duration"
|
||||
unit: "Unit"
|
||||
second: "Seconds"
|
||||
minute: "Minutes"
|
||||
hour: "Hours"
|
||||
day: "S"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Send a reaction"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -520,7 +540,7 @@ common/views/components/signin.vue:
|
||||
signin-with-twitter: "Log in with Twitter"
|
||||
signin-with-github: "Sign in with GitHub"
|
||||
signin-with-discord: "Sign in with Discord"
|
||||
login-failed: "Log in failed. Make sure you have entered your correct username and password."
|
||||
login-failed: "Logging in has failed. Make sure you have entered the correct username and password."
|
||||
common/views/components/signup.vue:
|
||||
invitation-code: "Invitation code"
|
||||
invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>."
|
||||
@ -628,12 +648,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "Your email has been verified."
|
||||
email-not-verified: "Email address is not confirmed. Please check your inbox."
|
||||
export: "Export"
|
||||
import: "Import"
|
||||
export-and-import: "Export and Import"
|
||||
export-targets:
|
||||
all-notes: "All posted Notes"
|
||||
following-list: "List of followers"
|
||||
mute-list: "List of muted accounts"
|
||||
blocking-list: "List of blocked accounts"
|
||||
user-lists: "Lists"
|
||||
export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
|
||||
import-requested: "You have initiated an import. This may take quite some time."
|
||||
enter-password: "Please enter your password"
|
||||
danger-zone: "Cautious options"
|
||||
delete-account: "Remove the account"
|
||||
@ -1196,7 +1220,7 @@ admin/views/users.vue:
|
||||
updatedAtAsc: "Last Updated (Ascending)"
|
||||
updatedAtDesc: "Last Updated (Descending)"
|
||||
state:
|
||||
title: "Status"
|
||||
title: "Sort"
|
||||
all: "All"
|
||||
admin: "Administrator"
|
||||
moderator: "Moderator"
|
||||
@ -1257,7 +1281,7 @@ admin/views/federation.vue:
|
||||
users: "Users"
|
||||
following: "Following"
|
||||
followers: "Followers"
|
||||
status: "Status"
|
||||
status: "Statuses"
|
||||
latest-request-sent-at: "Time of last request sent"
|
||||
latest-request-received-at: "Last request received at"
|
||||
remove-all-following: "Withold all followers"
|
||||
@ -1273,19 +1297,19 @@ admin/views/federation.vue:
|
||||
caughtAtDesc: "Date of discovery (Descending)"
|
||||
lastCommunicatedAtAsc: "The date and time of the older interactions"
|
||||
lastCommunicatedAtDesc: "The date and time of the newer interactions"
|
||||
notesAsc: "Order by least Notes posted"
|
||||
notesDesc: "Order by most Notes posted"
|
||||
notesAsc: "Least Notes posted"
|
||||
notesDesc: "Most Notes posted"
|
||||
usersAsc: "Less followers"
|
||||
usersDesc: "More followers"
|
||||
followingAsc: "Least followed"
|
||||
followingDesc: "Has more followers"
|
||||
followersAsc: "Sort by having less followers"
|
||||
followersDesc: "Sort by the larger number of followers"
|
||||
followingDesc: "Most followed"
|
||||
followersAsc: "Having less followers"
|
||||
followersDesc: "The largest number of followers"
|
||||
driveUsageAsc: "Least storage used"
|
||||
driveUsageDesc: "Most storage used"
|
||||
driveFilesAsc: "By the smallest number of files stored on Drive"
|
||||
driveFilesDesc: "By the largest number of files stored on Drive"
|
||||
state: "Status"
|
||||
driveFilesAsc: "Least files stored on Drive"
|
||||
driveFilesDesc: "The largest number of files stored on Drive"
|
||||
state: "Sort"
|
||||
states:
|
||||
all: "All"
|
||||
blocked: "Blocked"
|
||||
@ -1328,8 +1352,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "Search feature is turned off in the settings for this instance."
|
||||
not-found: "No posts were found for '{q}'"
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Share on {name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "No posts contains \"{q}\" found."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
@ -1353,7 +1375,7 @@ desktop/views/pages/user/user.header.vue:
|
||||
following: "Following"
|
||||
followers: "Followers"
|
||||
is-bot: "This account is a Bot"
|
||||
no-description: "The user has not written their profile introduction"
|
||||
no-description: "This user has not written their profile introduction"
|
||||
years-old: "{age} years old"
|
||||
year: "/"
|
||||
month: "/"
|
||||
@ -1365,7 +1387,7 @@ desktop/views/pages/user/user.timeline.vue:
|
||||
with-media: "Media"
|
||||
my-posts: "My posts"
|
||||
desktop/views/widgets/messaging.vue:
|
||||
title: "Message"
|
||||
title: "Messaging"
|
||||
desktop/views/widgets/notifications.vue:
|
||||
title: "Notifications"
|
||||
desktop/views/widgets/polls.vue:
|
||||
|
@ -103,6 +103,32 @@ common:
|
||||
tags: "Etiquetas"
|
||||
blocking: "Bloquear"
|
||||
password: "Contraseña"
|
||||
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
|
||||
line-width: "Grosor de línea"
|
||||
line-width-thick: "Grosor"
|
||||
font-size: "Tamaño del texto"
|
||||
font-size-x-small: "Muy pequeño"
|
||||
font-size-small: "Pequeño"
|
||||
font-size-medium: "Normal"
|
||||
font-size-large: "Grande"
|
||||
font-size-x-large: "Muy grande"
|
||||
deck-column-align: "Alineamiento de las columnas"
|
||||
deck-column-align-center: "Centrar"
|
||||
deck-column-align-left: "Izquierda"
|
||||
deck-column-align-flexible: "Flexible"
|
||||
deck-column-width: "Ancho de las columnas"
|
||||
deck-column-width-narrow: "Estrecho"
|
||||
deck-column-width-narrower: "Un poco estrecho"
|
||||
deck-column-width-normal: "Normal"
|
||||
deck-column-width-wider: "Un poco ancho"
|
||||
deck-column-width-wide: "Ancho"
|
||||
use-shadow: "Usar sombras en la Interfaz de Usuario"
|
||||
rounded-corners: "Esquinas redondeadas en la Interfaz de Usuario"
|
||||
circle-icons: "Usar iconos circulares"
|
||||
contrasted-acct: "Añadir contraste al nombre de usuario"
|
||||
wallpaper: "Fondo de pantalla"
|
||||
choose-wallpaper: "Escoge un fondo de pantalla"
|
||||
navbar-position-left: "Izquierda"
|
||||
search: "Buscar"
|
||||
delete: "eliminar"
|
||||
loading: "cargando"
|
||||
@ -303,7 +329,6 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "'{}' para votar"
|
||||
vote-count: "{} votos"
|
||||
total-users: "{} usuario(s) que ha(n) votado"
|
||||
vote: "Vota"
|
||||
show-result: "Mostrar resultados"
|
||||
voted: "Votado"
|
||||
@ -313,6 +338,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "Borra la opción"
|
||||
add: "+ Añade una opción"
|
||||
destroy: "Cancelar la encuesta"
|
||||
day: "domingo"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Escoge una reacción"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -395,9 +421,11 @@ common/views/components/profile-editor.vue:
|
||||
save: "Guardar"
|
||||
email-address: "Correo electrónico"
|
||||
export: "Exportar"
|
||||
import: "Importar"
|
||||
export-targets:
|
||||
mute-list: "Silenciar"
|
||||
blocking-list: "Bloquear"
|
||||
user-lists: "Listas"
|
||||
enter-password: "Escribe una contraseña"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Usuarios"
|
||||
|
@ -383,7 +383,6 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Voter pour '{}'"
|
||||
vote-count: "{} votes"
|
||||
total-users: "{} utilisateur·rice·s ont voté"
|
||||
vote: "Vote"
|
||||
show-result: "Montrer les résultats"
|
||||
voted: "Voté"
|
||||
@ -393,6 +392,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "Supprimer ce choix"
|
||||
add: "+ Ajouter un choix"
|
||||
destroy: "Annuler ce sondage"
|
||||
day: "D"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Choisissez votre réaction"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -522,11 +522,13 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "L’adresse du courrier électronique a été vérifiée."
|
||||
email-not-verified: "Adresse de courriel n’est pas confirmée. Veuillez vérifier votre boite de réception."
|
||||
export: "Exporter"
|
||||
import: "Importer"
|
||||
export-targets:
|
||||
all-notes: "Toutes les notes publiées"
|
||||
following-list: "Liste des abonnements"
|
||||
mute-list: "Liste des comptes mis en sourdine"
|
||||
blocking-list: "Liste des comptes bloqués"
|
||||
user-lists: "Listes"
|
||||
export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
|
||||
enter-password: "Veuillez saisir votre mot de passe"
|
||||
danger-zone: "Zone de danger"
|
||||
@ -1207,8 +1209,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "La fonction de recherche est désactivée dans les paramètres de l’instance."
|
||||
not-found: "Aucune publication trouvée pour « {q} »."
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Partager avec {name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "Aucune publication contenant « {q} » n’a été trouvée."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -5,22 +5,46 @@
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const langs = ['de-DE', 'en-US', 'fr-FR', 'ja-JP', 'ja-KS', 'pl-PL', 'es-ES', 'nl-NL', 'zh-CN', 'ko-KR'];
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
...c,
|
||||
...Object.entries(a)
|
||||
.filter(([k]) => c && typeof c[k] === 'object')
|
||||
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
||||
}), {});
|
||||
|
||||
const loadLocale = lang => yaml.safeLoad(fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
|
||||
const locales = langs
|
||||
.map(lang => [lang, loadLocale(lang)])
|
||||
.map(([lang, locale], _, locales) => {
|
||||
switch (lang) {
|
||||
case 'ja-JP': return [lang, locale];
|
||||
case 'en-US': return [lang, { ...locales['ja-JP'], ...locale }];
|
||||
default: return [lang, {
|
||||
...(lang.startsWith('ja-') ? {} : locales['en-US']),
|
||||
...locales['ja-JP'],
|
||||
...locale
|
||||
}];
|
||||
const languages = [
|
||||
'de-DE',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'fr-FR',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'ko-KR',
|
||||
'nl-NL',
|
||||
'pl-PL',
|
||||
'zh-CN',
|
||||
];
|
||||
|
||||
const primaries = {
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
};
|
||||
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.safeLoad(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8')) || {}, a), {});
|
||||
|
||||
module.exports = Object.entries(locales)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
case 'ja-JP': return v;
|
||||
case 'ja-KS':
|
||||
case 'en-US': return merge(locales['ja-JP'], v);
|
||||
default: return merge(
|
||||
locales['ja-JP'],
|
||||
locales['en-US'],
|
||||
locales[`${lang}-${primaries[lang]}`] || {},
|
||||
v
|
||||
);
|
||||
}
|
||||
})
|
||||
.map(([lang, locale]) => ({ [lang]: loadLocale(lang) }));
|
||||
|
||||
module.exports = locales.reduce((a, b) => ({ ...a, ...b }));
|
||||
})(), a), {});
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
meta:
|
||||
lang: "In Italiano"
|
||||
lang: "Italiano"
|
||||
common:
|
||||
misskey: "A ⭐ of the fediverse"
|
||||
about-title: "A ⭐ of the fediverse."
|
||||
|
@ -334,6 +334,7 @@ common/views/pages/explore.vue:
|
||||
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "プレイヤーを開く"
|
||||
disable-player: "プレイヤーを閉じる"
|
||||
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "ユーザーがいません"
|
||||
@ -527,10 +528,15 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "「{}」に投票する"
|
||||
vote-count: "{}票"
|
||||
total-users: "{}人が投票"
|
||||
total-votes: "計{}票"
|
||||
vote: "投票する"
|
||||
show-result: "結果を見る"
|
||||
voted: "投票済み"
|
||||
closed: "終了済み"
|
||||
remaining-days: "終了まであと{d}日{h}時間"
|
||||
remaining-hours: "終了まであと{h}時間{m}分"
|
||||
remaining-minutes: "終了まであと{m}分{s}秒"
|
||||
remaining-seconds: "終了まであと{s}秒"
|
||||
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
|
||||
@ -538,6 +544,20 @@ common/views/components/poll-editor.vue:
|
||||
remove: "この選択肢を削除"
|
||||
add: "+選択肢を追加"
|
||||
destroy: "アンケートを破棄"
|
||||
multiple: "複数回答可"
|
||||
expiration: "期限"
|
||||
infinite: "無期限"
|
||||
at: "日時指定"
|
||||
after: "経過指定"
|
||||
no-more: "これ以上追加できません"
|
||||
deadline-date: "期日"
|
||||
deadline-time: "時間"
|
||||
interval: "期間"
|
||||
unit: "単位"
|
||||
second: "秒"
|
||||
minute: "分"
|
||||
hour: "時間"
|
||||
day: "日"
|
||||
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "リアクションを選択"
|
||||
@ -682,12 +702,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "メールアドレスが確認されました"
|
||||
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
export-and-import: "エクスポートとインポート"
|
||||
export-targets:
|
||||
all-notes: "すべての投稿データ"
|
||||
following-list: "フォロー"
|
||||
mute-list: "ミュート"
|
||||
blocking-list: "ブロック"
|
||||
user-lists: "リスト"
|
||||
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
|
||||
import-requested: "インポートをリクエストしました。これには時間がかかる場合があります。"
|
||||
enter-password: "パスワードを入力してください"
|
||||
danger-zone: "危険な設定"
|
||||
delete-account: "アカウントを削除"
|
||||
@ -1156,7 +1180,7 @@ admin/views/dashboard.vue:
|
||||
federated: "連合"
|
||||
|
||||
admin/views/queue.vue:
|
||||
operation: "操作"
|
||||
title: "キュー"
|
||||
remove-all-jobs: "すべてのジョブをクリア"
|
||||
|
||||
admin/views/abuse.vue:
|
||||
@ -1467,9 +1491,6 @@ desktop/views/pages/search.vue:
|
||||
not-available: "検索機能はインスタンスの設定で無効になっています。"
|
||||
not-found: "「{q}」に関する投稿は見つかりませんでした。"
|
||||
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "{name}で共有"
|
||||
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
|
||||
|
||||
|
@ -344,7 +344,6 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "「{}」に投票や!"
|
||||
vote-count: "{}票"
|
||||
total-users: "{}人が投票"
|
||||
vote: "投票するで"
|
||||
show-result: "結果を見よか"
|
||||
voted: "投票済みや"
|
||||
@ -354,6 +353,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "この選択肢を消すで"
|
||||
add: "+選択肢を追加"
|
||||
destroy: "アンケートをほかそ"
|
||||
day: "日"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "リアクション、どれにするんや?"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -476,10 +476,12 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "このメールアドレスOKや!"
|
||||
email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
|
||||
export: "エクスポート"
|
||||
import: "インポート"
|
||||
export-targets:
|
||||
following-list: "フォロー"
|
||||
mute-list: "ミュート"
|
||||
blocking-list: "ブロック"
|
||||
user-lists: "リスト"
|
||||
enter-password: "パスワードを入れてや"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "ユーザー"
|
||||
|
@ -489,16 +489,35 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "\"{}\"에 투표하기"
|
||||
vote-count: "{}표"
|
||||
total-users: "{}명이 투표"
|
||||
total-votes: "총 {}표"
|
||||
vote: "투표하기"
|
||||
show-result: "결과 보기"
|
||||
voted: "투표함"
|
||||
closed: "종료됨"
|
||||
remaining-days: "종료까지 앞으로 {d}일 {h}시간"
|
||||
remaining-hours: "종료까지 앞으로 {h}시간 {m}분"
|
||||
remaining-minutes: "종료까지 앞으로 {m}분 {s}초"
|
||||
remaining-seconds: "종료까지 앞으로 {s}초"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "투표에는 선택지가 최소한 두 개 필요합니다"
|
||||
choice-n: "선택지 {}"
|
||||
remove: "이 선택지를 제거"
|
||||
add: "+선택지 추가"
|
||||
destroy: "투표 제거"
|
||||
multiple: "복수 응답 가능"
|
||||
expiration: "기한"
|
||||
infinite: "무기한"
|
||||
at: "일시 지정"
|
||||
after: "기간 지정"
|
||||
no-more: "더 이상 추가할 수 없습니다"
|
||||
deadline-date: "기한"
|
||||
deadline-time: "시간"
|
||||
interval: "기간"
|
||||
unit: "단위"
|
||||
second: "초"
|
||||
minute: "분"
|
||||
hour: "시간"
|
||||
day: "일"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "반응 선택"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -628,12 +647,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "매일 주소가 확인되었습니다"
|
||||
email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
|
||||
export: "내보내기"
|
||||
import: "가져오기"
|
||||
export-and-import: "내보내기와 가져오기"
|
||||
export-targets:
|
||||
all-notes: "모든 글 데이터"
|
||||
following-list: "팔로잉"
|
||||
mute-list: "뮤트"
|
||||
blocking-list: "차단"
|
||||
user-lists: "리스트"
|
||||
export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
|
||||
import-requested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
|
||||
enter-password: "비밀번호를 입력하여 주십시오"
|
||||
danger-zone: "위험한 설정"
|
||||
delete-account: "계정 삭제"
|
||||
@ -1328,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다."
|
||||
not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "{name}(으)로 공유"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
@ -131,7 +131,6 @@ common/views/components/note-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Stemmen op '{}'"
|
||||
vote-count: "{} stemmen"
|
||||
total-users: "{} gebruikers hebben gestemd"
|
||||
vote: "Stemmen"
|
||||
show-result: "Resultaten tonen"
|
||||
voted: "Gestemd"
|
||||
@ -141,6 +140,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "Deze keuze verwijderen"
|
||||
add: "+ Keuze toevoegen"
|
||||
destroy: "Deze peiling vernietigen"
|
||||
day: "Z"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Kies een reactie"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -196,6 +196,7 @@ common/views/components/profile-editor.vue:
|
||||
banner: "Omslagfoto"
|
||||
export-targets:
|
||||
following-list: "Volgend"
|
||||
user-lists: "Lijsten"
|
||||
enter-password: "Voer het wachtwoord in"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Gebruiker"
|
||||
|
@ -151,6 +151,7 @@ common/views/components/poll.vue:
|
||||
voted: "Stemt"
|
||||
common/views/components/poll-editor.vue:
|
||||
choice-n: "Valg {}"
|
||||
day: "S"
|
||||
common/views/components/signin.vue:
|
||||
username: "Brukernavn"
|
||||
password: "Passord"
|
||||
@ -186,6 +187,7 @@ common/views/components/profile-editor.vue:
|
||||
save: "Lagre"
|
||||
export-targets:
|
||||
following-list: "Følger"
|
||||
user-lists: "Lister"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Bruker"
|
||||
common/views/widgets/broadcast.vue:
|
||||
|
@ -26,6 +26,7 @@ common:
|
||||
dark-mode: "Tryb ciemny"
|
||||
signin: "Zaloguj się"
|
||||
signup: "Rejestracja"
|
||||
signout: "Wyloguj się"
|
||||
got-it: "Rozumiem!"
|
||||
customization-tips:
|
||||
title: "Wskazówki o dostosowywaniu"
|
||||
@ -120,7 +121,24 @@ common:
|
||||
other: "Inne"
|
||||
appearance: "Wygląd"
|
||||
behavior: "Zachowanie"
|
||||
note-visibility: "Widoczność wpisów"
|
||||
line-width-thin: "Cienka"
|
||||
line-width-normal: "Normalna"
|
||||
line-width-thick: "Gruba"
|
||||
font-size: "Rozmiar tekstu"
|
||||
font-size-medium: "Normalna"
|
||||
font-size-x-large: "Duży"
|
||||
deck-column-align-center: "Po środku"
|
||||
deck-column-align-left: "Z lewej"
|
||||
deck-column-align-flexible: "Elastyczne"
|
||||
deck-column-width: "Szerokość kolumn w talii"
|
||||
deck-column-width-narrow: "Wąska"
|
||||
deck-column-width-narrower: "Trochę wąska"
|
||||
deck-column-width-normal: "Normalna"
|
||||
deck-column-width-wider: "Trochę szerokie"
|
||||
deck-column-width-wide: "Szeroka"
|
||||
timeline: "Oś czasu"
|
||||
navbar-position-left: "Z lewej"
|
||||
search: "Szukaj"
|
||||
delete: "Usuń"
|
||||
loading: "Ładowanie"
|
||||
@ -346,7 +364,6 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Zagłosuj na '{}'"
|
||||
vote-count: "{} głosów"
|
||||
total-users: "{} głosujących"
|
||||
vote: "Zagłosuj"
|
||||
show-result: "Pokaż wyniki"
|
||||
voted: "Zagłosowano"
|
||||
@ -356,6 +373,7 @@ common/views/components/poll-editor.vue:
|
||||
remove: "Usuń tą opcję"
|
||||
add: "+ Dodaj opcję"
|
||||
destroy: "Usuń tę ankietę"
|
||||
day: "N"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Wybierz reakcję"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -472,10 +490,12 @@ common/views/components/profile-editor.vue:
|
||||
email-address: "Adres e-mail"
|
||||
email-verified: "Twój adres e-mail został zweryfikowany."
|
||||
export: "Eksportuj"
|
||||
import: "Importuj"
|
||||
export-targets:
|
||||
following-list: "Śledzeni"
|
||||
mute-list: "Wycisz"
|
||||
blocking-list: "Zablokuj"
|
||||
user-lists: "Listy"
|
||||
enter-password: "Wprowadź hasło"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Użytkownicy"
|
||||
|
@ -158,6 +158,8 @@ common/views/components/messaging.vue:
|
||||
you: "Você"
|
||||
common/views/components/note-menu.vue:
|
||||
delete: "Apagar"
|
||||
common/views/components/poll-editor.vue:
|
||||
day: "Dom"
|
||||
common/views/components/visibility-chooser.vue:
|
||||
followers: "Seguidores"
|
||||
common/views/components/profile-editor.vue:
|
||||
|
@ -127,6 +127,8 @@ common/views/components/connect-failed.vue:
|
||||
title: "Невозможно подключиться к серверу"
|
||||
common/views/components/cw-button.vue:
|
||||
poll: "Голосования"
|
||||
common/views/components/poll-editor.vue:
|
||||
day: "Вс"
|
||||
common/views/widgets/memo.vue:
|
||||
title: "Заметка"
|
||||
desktop/views/components/sub-note-content.vue:
|
||||
|
@ -489,16 +489,35 @@ common/views/components/user-menu.vue:
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "为\"{}\"投票"
|
||||
vote-count: "{}票"
|
||||
total-users: "{} 人投票"
|
||||
total-votes: "总票数{}"
|
||||
vote: "投票"
|
||||
show-result: "显示结果"
|
||||
voted: "已投票"
|
||||
closed: "已截止"
|
||||
remaining-days: "{d}天{h}小时后截止"
|
||||
remaining-hours: "{h}小时{m}分后截止"
|
||||
remaining-minutes: "{m}分{s}秒后截止"
|
||||
remaining-seconds: "{s}秒后截止"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "至少选择两个选项"
|
||||
choice-n: "选择{}"
|
||||
remove: "删除选项"
|
||||
add: "+添加一个选项"
|
||||
destroy: "放弃投票"
|
||||
multiple: "允许多个投票"
|
||||
expiration: "截止时间"
|
||||
infinite: "永久"
|
||||
at: "指定日期"
|
||||
after: "指定时间"
|
||||
no-more: "最多只能添加十个回答"
|
||||
deadline-date: "日期"
|
||||
deadline-time: "时间"
|
||||
interval: "时长"
|
||||
unit: "单位"
|
||||
second: "秒"
|
||||
minute: "分"
|
||||
hour: "小时"
|
||||
day: "日"
|
||||
common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "选择回应"
|
||||
common/views/components/emoji-picker.vue:
|
||||
@ -628,12 +647,16 @@ common/views/components/profile-editor.vue:
|
||||
email-verified: "电子邮件地址已验证"
|
||||
email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
|
||||
export: "导出"
|
||||
import: "导入"
|
||||
export-and-import: "导出/导入"
|
||||
export-targets:
|
||||
all-notes: "所有发帖"
|
||||
following-list: "关注列表"
|
||||
mute-list: "屏蔽列表"
|
||||
blocking-list: "黑名单"
|
||||
user-lists: "列表"
|
||||
export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
|
||||
import-requested: "导入请求已提交。这可能需要花一点时间。"
|
||||
enter-password: "请输入您的密码"
|
||||
danger-zone: "危险选项"
|
||||
delete-account: "删除帐户"
|
||||
@ -1328,8 +1351,6 @@ desktop/views/pages/selectdrive.vue:
|
||||
desktop/views/pages/search.vue:
|
||||
not-available: "在此实例的设置中关闭搜索功能。"
|
||||
not-found: "没有找到“{q}”的帖子"
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "共享{name}"
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "没有找到带有主题标签“{q}”的帖子"
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
|
88
locales/zh-TW.yml
Normal file
88
locales/zh-TW.yml
Normal 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: "雲端硬碟"
|
60
package.json
60
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.91.2",
|
||||
"version": "10.93.1",
|
||||
"codename": "nighthike",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -32,6 +32,7 @@
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"@prezzemolo/zip": "0.0.3",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.5.8",
|
||||
"@types/chai-http": "3.0.5",
|
||||
"@types/dateformat": "3.0.0",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
@ -45,7 +46,6 @@
|
||||
"@types/gulp-uglify": "3.0.6",
|
||||
"@types/gulp-util": "3.0.34",
|
||||
"@types/is-root": "1.0.0",
|
||||
"@types/is-svg": "3.0.0",
|
||||
"@types/is-url": "1.2.28",
|
||||
"@types/js-yaml": "3.12.0",
|
||||
"@types/jsdom": "12.2.3",
|
||||
@ -58,7 +58,7 @@
|
||||
"@types/koa-logger": "3.1.1",
|
||||
"@types/koa-mount": "3.0.1",
|
||||
"@types/koa-multer": "1.0.0",
|
||||
"@types/koa-router": "7.0.39",
|
||||
"@types/koa-router": "7.0.40",
|
||||
"@types/koa-send": "4.1.1",
|
||||
"@types/koa-views": "2.0.3",
|
||||
"@types/koa__cors": "2.2.3",
|
||||
@ -66,7 +66,7 @@
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/mocha": "5.2.5",
|
||||
"@types/mongodb": "3.1.20",
|
||||
"@types/node": "10.12.24",
|
||||
"@types/node": "11.10.4",
|
||||
"@types/nodemailer": "4.6.6",
|
||||
"@types/nprogress": "0.0.29",
|
||||
"@types/oauth": "0.9.1",
|
||||
@ -84,7 +84,7 @@
|
||||
"@types/seedrandom": "2.4.27",
|
||||
"@types/sharp": "0.21.2",
|
||||
"@types/showdown": "1.9.2",
|
||||
"@types/speakeasy": "2.0.3",
|
||||
"@types/speakeasy": "2.0.4",
|
||||
"@types/systeminformation": "3.23.1",
|
||||
"@types/tinycolor2": "1.4.1",
|
||||
"@types/tmp": "0.0.33",
|
||||
@ -95,34 +95,34 @@
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.1",
|
||||
"animejs": "3.0.1",
|
||||
"apexcharts": "3.5.0",
|
||||
"apexcharts": "3.6.2",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bee-queue": "1.2.2",
|
||||
"bootstrap-vue": "2.0.0-rc.11",
|
||||
"bootstrap-vue": "2.0.0-rc.13",
|
||||
"bull": "3.7.0",
|
||||
"cafy": "15.1.0",
|
||||
"chai": "4.2.0",
|
||||
"chai-http": "4.2.1",
|
||||
"chalk": "2.4.2",
|
||||
"commander": "2.19.0",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "2.1.0",
|
||||
"css-loader": "2.1.1",
|
||||
"cssnano": "4.1.10",
|
||||
"dateformat": "3.0.3",
|
||||
"deep-equal": "1.0.1",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "1.0.0",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"elasticsearch": "15.3.1",
|
||||
"elasticsearch": "15.4.1",
|
||||
"emojilib": "2.4.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "5.12.0",
|
||||
"eslint": "5.15.1",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
"eventemitter3": "3.1.0",
|
||||
"feed": "2.0.2",
|
||||
"file-type": "10.7.1",
|
||||
"feed": "2.0.4",
|
||||
"file-type": "10.9.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gulp": "4.0.0",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
@ -130,21 +130,20 @@
|
||||
"gulp-mocha": "6.0.0",
|
||||
"gulp-rename": "1.4.0",
|
||||
"gulp-replace": "1.0.0",
|
||||
"gulp-sourcemaps": "2.6.4",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-stylus": "2.7.0",
|
||||
"gulp-tslint": "8.1.3",
|
||||
"gulp-tslint": "8.1.4",
|
||||
"gulp-typescript": "5.0.0",
|
||||
"gulp-uglify": "3.0.1",
|
||||
"gulp-uglify": "3.0.2",
|
||||
"gulp-util": "3.0.8",
|
||||
"gulp-yaml": "2.0.3",
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
"html-minifier": "3.5.21",
|
||||
"http-signature": "1.2.0",
|
||||
"insert-text-at-cursor": "0.1.2",
|
||||
"is-root": "2.0.0",
|
||||
"is-svg": "3.0.0",
|
||||
"js-yaml": "3.12.1",
|
||||
"jsdom": "13.2.0",
|
||||
"is-svg": "4.0.0",
|
||||
"js-yaml": "3.12.2",
|
||||
"jsdom": "14.0.0",
|
||||
"json5": "2.1.0",
|
||||
"json5-loader": "1.0.1",
|
||||
"katex": "0.10.1",
|
||||
@ -176,7 +175,6 @@
|
||||
"nodemailer": "5.1.1",
|
||||
"nprogress": "0.2.0",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"on-build-webpack": "0.1.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.1.0",
|
||||
"parsimmon": "1.12.0",
|
||||
@ -190,13 +188,13 @@
|
||||
"pug": "2.0.3",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.3.3",
|
||||
"randomcolor": "0.5.3",
|
||||
"ratelimiter": "3.2.0",
|
||||
"randomcolor": "0.5.4",
|
||||
"ratelimiter": "3.3.0",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "4.1.10",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.88.0",
|
||||
"request-promise-native": "1.0.5",
|
||||
"request-promise-native": "1.0.7",
|
||||
"request-stats": "3.0.0",
|
||||
"rimraf": "2.6.3",
|
||||
"rndstr": "1.0.0",
|
||||
@ -211,14 +209,14 @@
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.2.0",
|
||||
"systeminformation": "4.0.14",
|
||||
"systeminformation": "4.0.16",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"terser-webpack-plugin": "1.2.3",
|
||||
"textarea-caret": "3.1.0",
|
||||
"tinycolor2": "1.4.1",
|
||||
"tmp": "0.0.33",
|
||||
"ts-loader": "5.3.3",
|
||||
"ts-node": "8.0.2",
|
||||
"ts-node": "8.0.3",
|
||||
"tslint": "5.13.1",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"typescript": "3.3.3333",
|
||||
@ -233,7 +231,7 @@
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.5.3",
|
||||
"vue-cropperjs": "3.0.0",
|
||||
"vue-i18n": "8.8.2",
|
||||
"vue-i18n": "8.9.0",
|
||||
"vue-js-modal": "1.3.28",
|
||||
"vue-json-pretty": "1.4.1",
|
||||
"vue-loader": "15.7.0",
|
||||
@ -242,18 +240,18 @@
|
||||
"vue-router": "3.0.2",
|
||||
"vue-sequential-entrance": "1.1.3",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader": "1.2.12",
|
||||
"vue-svg-inline-loader": "1.2.13",
|
||||
"vue-template-compiler": "2.6.8",
|
||||
"vuedraggable": "2.18.1",
|
||||
"vuedraggable": "2.19.2",
|
||||
"vuewordcloud": "18.7.11",
|
||||
"vuex": "3.1.0",
|
||||
"vuex-persistedstate": "2.5.4",
|
||||
"web-push": "3.3.3",
|
||||
"webfinger.js": "2.7.0",
|
||||
"webpack": "4.28.4",
|
||||
"webpack-cli": "3.2.1",
|
||||
"webpack-cli": "3.2.3",
|
||||
"websocket": "1.0.28",
|
||||
"ws": "6.1.4",
|
||||
"ws": "6.2.0",
|
||||
"xev": "2.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ program
|
||||
.version(pkg.version)
|
||||
.option('--no-daemons', 'Disable daemon processes (for debbuging)')
|
||||
.option('--disable-clustering', 'Disable clustering')
|
||||
.option('--disable-queue', 'Disable job queue processing')
|
||||
.option('--only-server', 'Run server only (without job queue)')
|
||||
.option('--only-server', 'Run server only (without job queue processing)')
|
||||
.option('--only-queue', 'Pocessing job queue only (without server)')
|
||||
.option('--quiet', 'Suppress all logs')
|
||||
.option('--verbose', 'Enable all logs')
|
||||
@ -15,7 +14,6 @@ program
|
||||
.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.')
|
||||
.parse(process.argv);
|
||||
|
||||
/*if (process.env.MK_DISABLE_QUEUE)*/ program.disableQueue = true;
|
||||
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
|
||||
|
||||
export { program };
|
||||
|
194
src/client/app/admin/views/dashboard.queue-charts.vue
Normal file
194
src/client/app/admin/views/dashboard.queue-charts.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="mzxlfysy">
|
||||
<div>
|
||||
<header>
|
||||
<span><fa :icon="faInbox"/> In</span>
|
||||
<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
|
||||
</header>
|
||||
<div ref="in"></div>
|
||||
</div>
|
||||
<div>
|
||||
<header>
|
||||
<span><fa :icon="faPaperPlane"/> Out</span>
|
||||
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
|
||||
</header>
|
||||
<div ref="out"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faInbox } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
stats: [],
|
||||
inChart: null,
|
||||
outChart: null,
|
||||
faInbox, faPaperPlane
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
latestStats(): any {
|
||||
return this.stats[this.stats.length - 1];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
stats(stats) {
|
||||
this.inChart.updateSeries([{
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.outChart.updateSeries([{
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const chartOpts = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 200,
|
||||
animations: {
|
||||
dynamicAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
|
||||
series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
|
||||
xaxis: {
|
||||
type: 'numeric',
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
min: 0,
|
||||
}
|
||||
};
|
||||
|
||||
this.inChart = new ApexCharts(this.$refs.in, chartOpts);
|
||||
this.outChart = new ApexCharts(this.$refs.out, chartOpts);
|
||||
|
||||
this.inChart.render();
|
||||
this.outChart.render();
|
||||
|
||||
const connection = this.$root.stream.useSharedConnection('queueStats');
|
||||
connection.on('stats', this.onStats);
|
||||
connection.on('statsLog', this.onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 100
|
||||
});
|
||||
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
connection.dispose();
|
||||
this.inChart.destroy();
|
||||
this.outChart.destroy();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 100) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mzxlfysy
|
||||
display flex
|
||||
|
||||
> div
|
||||
display block
|
||||
flex 1
|
||||
padding 20px 12px 0 12px
|
||||
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
|
||||
background var(--face)
|
||||
border-radius 8px
|
||||
|
||||
&:first-child
|
||||
margin-right 16px
|
||||
|
||||
> header
|
||||
display flex
|
||||
padding 0 8px
|
||||
margin-bottom -16px
|
||||
color var(--adminDashboardCardFg)
|
||||
font-size 14px
|
||||
|
||||
> span
|
||||
&:last-child
|
||||
margin-left auto
|
||||
opacity 0.7
|
||||
|
||||
> span
|
||||
opacity 0.7
|
||||
|
||||
> div
|
||||
margin-bottom -10px
|
||||
|
||||
@media (max-width 1000px)
|
||||
display block
|
||||
margin-bottom 26px
|
||||
|
||||
> div
|
||||
&:first-child
|
||||
margin-right 0
|
||||
margin-bottom 26px
|
||||
|
||||
</style>
|
@ -73,6 +73,10 @@
|
||||
<x-charts ref="charts"/>
|
||||
</div>
|
||||
|
||||
<div class="queue">
|
||||
<x-queue/>
|
||||
</div>
|
||||
|
||||
<div class="cpu-memory">
|
||||
<x-cpu-memory :connection="connection"/>
|
||||
</div>
|
||||
@ -86,7 +90,8 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import XCpuMemory from "./cpu-memory.vue";
|
||||
import XCpuMemory from "./dashboard.cpu-memory.vue";
|
||||
import XQueue from "./dashboard.queue-charts.vue";
|
||||
import XCharts from "./charts.vue";
|
||||
import XApLog from "./ap-log.vue";
|
||||
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
|
||||
@ -98,6 +103,7 @@ export default Vue.extend({
|
||||
|
||||
components: {
|
||||
XCpuMemory,
|
||||
XQueue,
|
||||
XCharts,
|
||||
XApLog,
|
||||
MarqueeText
|
||||
@ -274,6 +280,9 @@ export default Vue.extend({
|
||||
> .charts
|
||||
margin-bottom 16px
|
||||
|
||||
> .queue
|
||||
margin-bottom 16px
|
||||
|
||||
> .cpu-memory
|
||||
margin-bottom 16px
|
||||
|
||||
|
@ -85,11 +85,10 @@ export default Vue.extend({
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.nqjzuvev
|
||||
white-space nowrap
|
||||
overflow auto
|
||||
padding 8px
|
||||
background #000
|
||||
color #fff
|
||||
font-size 14px
|
||||
|
||||
> code
|
||||
display block
|
||||
|
@ -1,7 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<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.activeSincePrevTick | number" type="text" readonly>
|
||||
<span>Process</span>
|
||||
<template #prefix><fa :icon="fasPlayCircle"/></template>
|
||||
<template #suffix>jobs/s</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
<template #prefix><fa :icon="farPlayCircle"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
|
||||
<span>Waiting</span>
|
||||
<template #prefix><fa :icon="faStopCircle"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
|
||||
<span>Delayed</span>
|
||||
<template #prefix><fa :icon="faStopwatch"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<div ref="deliverChart" class="chart"></div>
|
||||
</section>
|
||||
<section class="wptihjuy">
|
||||
<header><fa :icon="faInbox"/> Inbox</header>
|
||||
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
|
||||
<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
|
||||
<span>Process</span>
|
||||
<template #prefix><fa :icon="fasPlayCircle"/></template>
|
||||
<template #suffix>jobs/s</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
<template #prefix><fa :icon="farPlayCircle"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
|
||||
<span>Waiting</span>
|
||||
<template #prefix><fa :icon="faStopCircle"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
|
||||
<span>Delayed</span>
|
||||
<template #prefix><fa :icon="faStopwatch"/></template>
|
||||
<template #suffix>jobs</template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<div ref="inboxChart" class="chart"></div>
|
||||
</section>
|
||||
<section>
|
||||
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
|
||||
</section>
|
||||
@ -12,15 +64,134 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import ApexCharts from 'apexcharts';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/queue.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
stats: [],
|
||||
deliverChart: null,
|
||||
inboxChart: null,
|
||||
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
latestStats(): any {
|
||||
return this.stats[this.stats.length - 1];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
stats(stats) {
|
||||
this.inboxChart.updateSeries([{
|
||||
name: 'Process',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
|
||||
}, {
|
||||
name: 'Active',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
|
||||
}, {
|
||||
name: 'Waiting',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.deliverChart.updateSeries([{
|
||||
name: 'Process',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
|
||||
}, {
|
||||
name: 'Active',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
|
||||
}, {
|
||||
name: 'Waiting',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const chartOpts = {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 200,
|
||||
animations: {
|
||||
dynamicAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
},
|
||||
},
|
||||
series: [] as any,
|
||||
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
|
||||
xaxis: {
|
||||
type: 'numeric',
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
min: 0,
|
||||
}
|
||||
};
|
||||
|
||||
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: {
|
||||
async removeAllJobs() {
|
||||
const process = async () => {
|
||||
@ -38,6 +209,24 @@ export default Vue.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 100) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.wptihjuy
|
||||
> .chart
|
||||
min-height 200px !important
|
||||
|
||||
</style>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
|
||||
<img class="avatar" :src="user.avatarUrl" alt=""/>
|
||||
<span class="name">
|
||||
<mk-user-name :user="user"/>
|
||||
<mk-user-name :user="user" :key="user.id"/>
|
||||
</span>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
</li>
|
||||
|
@ -3,32 +3,31 @@
|
||||
<header>
|
||||
<button v-for="category in categories"
|
||||
:title="category.text"
|
||||
@click="go(category.ref)"
|
||||
@click="go(category)"
|
||||
:class="{ active: category.isActive }"
|
||||
>
|
||||
<fa :icon="category.icon" fixed-width/>
|
||||
</button>
|
||||
</header>
|
||||
<div class="emojis" ref="emojis" @scroll.passive="onScroll">
|
||||
<section v-for="category in categories" :ref="category.ref">
|
||||
<header><fa :icon="category.icon" fixed-width/> {{ category.text }}</header>
|
||||
<div v-if="category.name">
|
||||
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === category.name)"
|
||||
:title="emoji[0]"
|
||||
@click="chosen(emoji[1].char)"
|
||||
>
|
||||
<mk-emoji :emoji="emoji[1].char"/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button v-for="emoji in customEmojis"
|
||||
:title="emoji.name"
|
||||
@click="chosen(`:${emoji.name}:`)"
|
||||
>
|
||||
<img :src="emoji.url" :alt="emoji.name"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="emojis">
|
||||
<header><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
|
||||
<div v-if="categories.find(x => x.isActive).name">
|
||||
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === categories.find(x => x.isActive).name)"
|
||||
:title="emoji[0]"
|
||||
@click="chosen(emoji[1].char)"
|
||||
:key="emoji[0]"
|
||||
>
|
||||
<mk-emoji :emoji="emoji[1].char"/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button v-for="emoji in customEmojis"
|
||||
:title="emoji.name"
|
||||
@click="chosen(`:${emoji.name}:`)"
|
||||
>
|
||||
<img :src="emoji.url" :alt="emoji.name"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -48,55 +47,46 @@ export default Vue.extend({
|
||||
lib,
|
||||
customEmojis: [],
|
||||
categories: [{
|
||||
ref: 'customEmojiSection',
|
||||
text: this.$t('custom-emoji'),
|
||||
icon: faAsterisk,
|
||||
isActive: true
|
||||
}, {
|
||||
name: 'people',
|
||||
ref: 'peopleSection',
|
||||
text: this.$t('people'),
|
||||
icon: ['far', 'laugh'],
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'animals_and_nature',
|
||||
ref: 'animalsAndNatureSection',
|
||||
text: this.$t('animals-and-nature'),
|
||||
icon: faLeaf,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'food_and_drink',
|
||||
ref: 'foodAndDrinkSection',
|
||||
text: this.$t('food-and-drink'),
|
||||
icon: faUtensils,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'activity',
|
||||
ref: 'activitySection',
|
||||
text: this.$t('activity'),
|
||||
icon: faFutbol,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'travel_and_places',
|
||||
ref: 'travelAndPlacesSection',
|
||||
text: this.$t('travel-and-places'),
|
||||
icon: faCity,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'objects',
|
||||
ref: 'objectsSection',
|
||||
text: this.$t('objects'),
|
||||
icon: faDice,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'symbols',
|
||||
ref: 'symbolsSection',
|
||||
text: this.$t('symbols'),
|
||||
icon: faHeart,
|
||||
isActive: false
|
||||
}, {
|
||||
name: 'flags',
|
||||
ref: 'flagsSection',
|
||||
text: this.$t('flags'),
|
||||
icon: faFlag,
|
||||
isActive: false
|
||||
@ -109,15 +99,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
go(ref) {
|
||||
this.$refs.emojis.scrollTop = this.$refs[ref][0].offsetTop;
|
||||
},
|
||||
|
||||
onScroll(e) {
|
||||
for (const x of this.categories) {
|
||||
const top = e.target.scrollTop;
|
||||
const el = this.$refs[x.ref][0];
|
||||
x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top;
|
||||
go(category) {
|
||||
for (const c of this.categories) {
|
||||
c.isActive = c.name === category.name;
|
||||
}
|
||||
},
|
||||
|
||||
@ -156,47 +140,46 @@ export default Vue.extend({
|
||||
overflow-y auto
|
||||
overflow-x hidden
|
||||
|
||||
> section
|
||||
> header
|
||||
position sticky
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
padding 8px
|
||||
background var(--faceHeader)
|
||||
color var(--text)
|
||||
font-size 12px
|
||||
> header
|
||||
position sticky
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
padding 8px
|
||||
background var(--faceHeader)
|
||||
color var(--text)
|
||||
font-size 12px
|
||||
|
||||
> div
|
||||
display grid
|
||||
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
|
||||
gap 4px
|
||||
padding 8px
|
||||
> div
|
||||
display grid
|
||||
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
|
||||
gap 4px
|
||||
padding 8px
|
||||
|
||||
> button
|
||||
padding 0
|
||||
width 100%
|
||||
> button
|
||||
padding 0
|
||||
width 100%
|
||||
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
width 1px
|
||||
height 0
|
||||
padding-bottom 100%
|
||||
|
||||
&:hover
|
||||
> *
|
||||
transform scale(1.2)
|
||||
transition transform 0s
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
width 1px
|
||||
height 0
|
||||
padding-bottom 100%
|
||||
|
||||
&:hover
|
||||
> *
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
font-size 28px
|
||||
transition transform 0.2s ease
|
||||
pointer-events none
|
||||
transform scale(1.2)
|
||||
transition transform 0s
|
||||
|
||||
> *
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
font-size 28px
|
||||
transition transform 0.2s ease
|
||||
pointer-events none
|
||||
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="View source on GitHub">
|
||||
<a class="a" :href="repo" target="_blank" title="View source on GitHub">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
|
||||
@ -8,9 +8,25 @@
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
repositoryUrl: 'https://github.com/syuilo/misskey'
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
if (meta.maintainer)
|
||||
this.repositoryUrl = meta.maintainer.repository_url;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.a
|
||||
display block
|
||||
|
||||
|
@ -5,17 +5,17 @@
|
||||
|
||||
<div style="overflow: hidden; line-height: 28px;">
|
||||
<p class="turn" v-if="!iAmPlayer && !game.isEnded">
|
||||
<mfm :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
|
||||
<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
|
||||
<mk-ellipsis/>
|
||||
</p>
|
||||
<p class="turn" v-if="logPos != logs.length">
|
||||
<mfm :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
|
||||
<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
|
||||
</p>
|
||||
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
|
||||
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
|
||||
<p class="result" v-if="game.isEnded && logPos == logs.length">
|
||||
<template v-if="game.winner">
|
||||
<mfm :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/>
|
||||
<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/>
|
||||
<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
|
||||
</template>
|
||||
<template v-else>{{ $t('@.reversi.drawn') }}</template>
|
||||
|
@ -12,21 +12,54 @@
|
||||
</li>
|
||||
</ul>
|
||||
<button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button>
|
||||
<button class="add" v-else disabled>{{ $t('no-more') }}</button>
|
||||
<button class="destroy" @click="destroy" :title="$t('destroy')">
|
||||
<fa icon="times"/>
|
||||
</button>
|
||||
<section>
|
||||
<ui-switch v-model="multiple">{{ $t('multiple') }}</ui-switch>
|
||||
<div>
|
||||
<ui-select v-model="expiration">
|
||||
<template #label>{{ $t('expiration') }}</template>
|
||||
<option value="infinite">{{ $t('infinite') }}</option>
|
||||
<option value="at">{{ $t('at') }}</option>
|
||||
<option value="after">{{ $t('after') }}</option>
|
||||
</ui-select>
|
||||
<section v-if="expiration === 'at'">
|
||||
<ui-input v-model="atDate" type="date">{{ $t('deadline-date') }}</ui-input>
|
||||
<ui-input v-model="atTime" type="time">{{ $t('deadline-time') }}</ui-input>
|
||||
</section>
|
||||
<section v-if="expiration === 'after'">
|
||||
<ui-input v-model="after" type="number">{{ $t('interval') }}</ui-input>
|
||||
<ui-select v-model="unit">
|
||||
<template #label>{{ $t('unit') }}</template>
|
||||
<option value="second">{{ $t('second') }}</option>
|
||||
<option value="minute">{{ $t('minute') }}</option>
|
||||
<option value="hour">{{ $t('hour') }}</option>
|
||||
<option value="day">{{ $t('day') }}</option>
|
||||
</ui-select>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as moment from 'moment';
|
||||
import i18n from '../../../i18n';
|
||||
import { erase } from '../../../../../prelude/array';
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/poll-editor.vue'),
|
||||
data() {
|
||||
return {
|
||||
choices: ['', '']
|
||||
choices: ['', ''],
|
||||
multiple: false,
|
||||
expiration: 'infinite',
|
||||
atDate: moment().add(1, 'day').toISOString().split('T')[0],
|
||||
atTime: '00:00',
|
||||
after: 0,
|
||||
unit: 'second'
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -55,15 +88,46 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
get() {
|
||||
const at = () => {
|
||||
const [date] = moment(this.atDate).toISOString().split('T');
|
||||
const [hour, minute] = this.atTime.split(':');
|
||||
return moment(`${date}T${hour}:${minute}Z`).valueOf();
|
||||
};
|
||||
|
||||
const after = () => {
|
||||
let base = parseInt(this.after);
|
||||
switch (this.unit) {
|
||||
case 'day': base *= 24;
|
||||
case 'hour': base *= 60;
|
||||
case 'minute': base *= 60;
|
||||
case 'second': return base *= 1000;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
choices: erase('', this.choices)
|
||||
}
|
||||
choices: erase('', this.choices),
|
||||
multiple: this.multiple,
|
||||
...(
|
||||
this.expiration === 'at' ? { expiresAt: at() } :
|
||||
this.expiration === 'after' ? { expiredAfter: after() } : {})
|
||||
};
|
||||
},
|
||||
|
||||
set(data) {
|
||||
if (data.choices.length == 0) return;
|
||||
this.choices = data.choices;
|
||||
if (data.choices.length == 1) this.choices = this.choices.concat('');
|
||||
this.multiple = data.multiple;
|
||||
if (data.expiresAt) {
|
||||
this.expiration = 'at';
|
||||
this.atDate = this.atTime = data.expiresAt;
|
||||
} else if (typeof data.expiredAfter === 'number') {
|
||||
this.expiration = 'after';
|
||||
this.after = data.expiredAfter;
|
||||
} else {
|
||||
this.expiration = 'infinite';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -128,6 +192,7 @@ export default Vue.extend({
|
||||
margin 8px 0 0 0
|
||||
vertical-align top
|
||||
color var(--primary)
|
||||
z-index 1
|
||||
|
||||
> .destroy
|
||||
position absolute
|
||||
@ -142,4 +207,23 @@ export default Vue.extend({
|
||||
&:active
|
||||
color var(--primaryDarken30)
|
||||
|
||||
> section
|
||||
margin 16px 0 -16px 0
|
||||
|
||||
> div
|
||||
margin 0 8px
|
||||
|
||||
&:last-child
|
||||
flex 1 0 auto
|
||||
|
||||
> section
|
||||
align-items center
|
||||
display flex
|
||||
margin -32px 0 0
|
||||
|
||||
> :first-child
|
||||
margin-right 16px
|
||||
|
||||
> .ui-input
|
||||
flex 1 0 auto
|
||||
</style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="mk-poll" :data-is-voted="isVoted">
|
||||
<div class="mk-poll" :data-done="closed || isVoted">
|
||||
<ul>
|
||||
<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
|
||||
<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
|
||||
<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
|
||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span>
|
||||
<template v-if="choice.isVoted"><fa icon="check"/></template>
|
||||
<mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/>
|
||||
@ -10,11 +10,13 @@
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="total > 0">
|
||||
<span>{{ $t('total-users').replace('{}', total) }}</span>
|
||||
<span>・</span>
|
||||
<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
|
||||
<p>
|
||||
<span>{{ $t('total-votes').replace('{}', total) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
|
||||
<span v-if="isVoted">{{ $t('voted') }}</span>
|
||||
<span v-else-if="closed">{{ $t('closed') }}</span>
|
||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -28,6 +30,7 @@ export default Vue.extend({
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
remaining: -1,
|
||||
showResult: false
|
||||
};
|
||||
},
|
||||
@ -38,19 +41,43 @@ export default Vue.extend({
|
||||
total(): number {
|
||||
return sum(this.poll.choices.map(x => x.votes));
|
||||
},
|
||||
closed(): boolean {
|
||||
return !this.remaining;
|
||||
},
|
||||
timer(): string {
|
||||
return this.$t(
|
||||
this.remaining > 86400 ? 'remaining-days' :
|
||||
this.remaining > 3600 ? 'remaining-hours' :
|
||||
this.remaining > 60 ? 'remaining-minutes' : 'remaining-seconds')
|
||||
.replace('{s}', Math.floor(this.remaining % 60))
|
||||
.replace('{m}', Math.floor(this.remaining / 60) % 60)
|
||||
.replace('{h}', Math.floor(this.remaining / 3600) % 24)
|
||||
.replace('{d}', Math.floor(this.remaining / 86400));
|
||||
},
|
||||
isVoted(): boolean {
|
||||
return this.poll.choices.some(c => c.isVoted);
|
||||
return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.showResult = this.isVoted;
|
||||
|
||||
if (this.note.poll.expiresAt) {
|
||||
const update = () => {
|
||||
if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
|
||||
requestAnimationFrame(update);
|
||||
else
|
||||
this.showResult = true;
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleShowResult() {
|
||||
this.showResult = !this.showResult;
|
||||
},
|
||||
vote(id) {
|
||||
if (this.poll.choices.some(c => c.isVoted)) return;
|
||||
if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
|
||||
this.$root.api('notes/polls/vote', {
|
||||
noteId: this.note.id,
|
||||
choice: id
|
||||
@ -61,7 +88,7 @@ export default Vue.extend({
|
||||
Vue.set(c, 'isVoted', true);
|
||||
}
|
||||
}
|
||||
this.showResult = true;
|
||||
if (!this.showResult) this.showResult = !this.poll.multiple;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -114,7 +141,7 @@ export default Vue.extend({
|
||||
a
|
||||
color inherit
|
||||
|
||||
&[data-is-voted]
|
||||
&[data-done]
|
||||
> ul > li
|
||||
cursor default
|
||||
|
||||
|
@ -51,12 +51,12 @@
|
||||
<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-button @click="save(true)">{{ $t('save') }}</ui-button>
|
||||
<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
|
||||
</ui-form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('advanced') }}</header>
|
||||
<header><fa :icon="faCogs"/> {{ $t('advanced') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
|
||||
@ -66,7 +66,7 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('privacy') }}</header>
|
||||
<header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
|
||||
@ -76,7 +76,7 @@
|
||||
</section>
|
||||
|
||||
<section v-if="enableEmail">
|
||||
<header>{{ $t('email') }}</header>
|
||||
<header><fa :icon="faEnvelope"/> {{ $t('email') }}</header>
|
||||
|
||||
<div>
|
||||
<template v-if="$store.state.i.email != null">
|
||||
@ -84,12 +84,12 @@
|
||||
<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
|
||||
</template>
|
||||
<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
|
||||
<ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
|
||||
<ui-button @click="updateEmail()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('export') }}</header>
|
||||
<header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-select v-model="exportTarget">
|
||||
@ -97,8 +97,12 @@
|
||||
<option value="following">{{ $t('export-targets.following-list') }}</option>
|
||||
<option value="mute">{{ $t('export-targets.mute-list') }}</option>
|
||||
<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
|
||||
<option value="user-lists">{{ $t('export-targets.user-lists') }}</option>
|
||||
</ui-select>
|
||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||
<ui-horizon-group class="fit-bottom">
|
||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||
<ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -118,7 +122,8 @@ import { apiUrl, host } from '../../../../config';
|
||||
import { toUnicode } from 'punycode';
|
||||
import langmap from 'langmap';
|
||||
import { unique } from '../../../../../../prelude/array';
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/profile-editor.vue'),
|
||||
@ -147,7 +152,7 @@ export default Vue.extend({
|
||||
avatarUploading: false,
|
||||
bannerUploading: false,
|
||||
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 == 'mute' ? 'i/export-mute' :
|
||||
this.exportTarget == 'blocking' ? 'i/export-blocking' :
|
||||
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
|
||||
null, {});
|
||||
|
||||
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() {
|
||||
const { canceled: canceled, result: password } = await this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
|
@ -386,7 +386,7 @@ export default Vue.extend({
|
||||
height: 50px;
|
||||
background-color: #83D8FF;
|
||||
border-radius: 90px - 6;
|
||||
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
|
||||
&:before {
|
||||
content: 'Light';
|
||||
@ -418,14 +418,14 @@ export default Vue.extend({
|
||||
background-color: #FFCF96;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.3);
|
||||
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
.crater {
|
||||
position: absolute;
|
||||
background-color: #E8CDA5;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
transition: opacity 200ms ease-in-out !important;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
@ -454,7 +454,7 @@ export default Vue.extend({
|
||||
.star {
|
||||
position: absolute;
|
||||
background-color: #ffffff;
|
||||
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@ -486,7 +486,7 @@ export default Vue.extend({
|
||||
.star--5,
|
||||
.star--6 {
|
||||
opacity: 0;
|
||||
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
|
||||
.star--4 {
|
||||
@ -559,13 +559,13 @@ export default Vue.extend({
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
.star--4 {
|
||||
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
.star--5 {
|
||||
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
.star--6 {
|
||||
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,6 +366,9 @@ root(fill)
|
||||
&[type='file']
|
||||
display none
|
||||
|
||||
&[type='number']
|
||||
text-align right
|
||||
|
||||
> .prefix
|
||||
> .suffix
|
||||
display block
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button>
|
||||
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
|
||||
</div>
|
||||
<div v-else-if="tweetUrl && detail" class="twitter">
|
||||
@ -126,6 +127,22 @@ export default Vue.extend({
|
||||
position relative
|
||||
width 100%
|
||||
|
||||
> button
|
||||
position absolute
|
||||
top -1.5em
|
||||
right 0
|
||||
font-size 1em
|
||||
width 1.5em
|
||||
height 1.5em
|
||||
padding 0
|
||||
margin 0
|
||||
color var(--text)
|
||||
background rgba(128, 128, 128, 0.2)
|
||||
opacity 0.7
|
||||
|
||||
&:hover
|
||||
opacity 0.9
|
||||
|
||||
> iframe
|
||||
height 100%
|
||||
left 0
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
|
||||
<div ref="chart" class="chart"></div>
|
||||
<x-hashtag-tl :tag-tl="tagTl" class="tl"/>
|
||||
<x-hashtag-tl :tag-tl="tagTl" class="tl" :key="JSON.stringify(tagTl)"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</template>
|
||||
|
@ -26,6 +26,7 @@
|
||||
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
</select>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h1>{{ $t('share-with', { name }) }}</h1>
|
||||
<div>
|
||||
<mk-signin v-if="!$store.getters.isSignedIn"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
|
||||
<p v-if="posted" class="posted"><fa icon="check"/></p>
|
||||
</div>
|
||||
<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
|
||||
@ -20,9 +20,21 @@ export default Vue.extend({
|
||||
return {
|
||||
name: null,
|
||||
posted: false,
|
||||
text: new URLSearchParams(location.search).get('text')
|
||||
text: new URLSearchParams(location.search).get('text'),
|
||||
url: new URLSearchParams(location.search).get('url'),
|
||||
title: new URLSearchParams(location.search).get('title'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
template(): string {
|
||||
let t = '';
|
||||
if (this.title && this.url) t += `【[${title}](${url})】\n`;
|
||||
if (this.title && !this.url) t += `【${title}】\n`;
|
||||
if (this.text) t += `${text}\n`;
|
||||
if (!this.title && this.url) t += `${url}`;
|
||||
return t.trim();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
window.close();
|
@ -31,3 +31,4 @@ Vue.component('mkw-version', wVersion);
|
||||
Vue.component('mkw-hashtags', wHashtags);
|
||||
Vue.component('mkw-instance', wInstance);
|
||||
Vue.component('mkw-post-form', wPostForm);
|
||||
Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default));
|
||||
|
162
src/client/app/common/views/widgets/queue.vue
Normal file
162
src/client/app/common/views/widgets/queue.vue
Normal file
@ -0,0 +1,162 @@
|
||||
<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.active }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.outChart.updateSeries([{
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
|
||||
}, {
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
|
||||
series: [{ data: [] }, { data: [] }, { data: [] }, { 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>
|
@ -18,7 +18,7 @@ import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||
import MkDrive from './views/pages/drive.vue';
|
||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||
import MkReversi from './views/pages/games/reversi.vue';
|
||||
import MkShare from './views/pages/share.vue';
|
||||
import MkShare from '../common/views/pages/share.vue';
|
||||
import MkFollow from '../common/views/pages/follow.vue';
|
||||
import MkNotFound from '../common/views/pages/not-found.vue';
|
||||
import MkSettings from './views/pages/settings.vue';
|
||||
|
@ -129,9 +129,9 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
this.$root.api('notes/replies', {
|
||||
this.$root.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
|
@ -123,9 +123,9 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
if (this.detail) {
|
||||
this.$root.api('notes/replies', {
|
||||
this.$root.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
|
@ -115,6 +115,8 @@ export default Vue.extend({
|
||||
uploadings: [],
|
||||
poll: false,
|
||||
pollChoices: [],
|
||||
pollMultiple: false,
|
||||
pollExpiration: [],
|
||||
useCw: false,
|
||||
cw: null,
|
||||
geo: null,
|
||||
@ -295,7 +297,10 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onPollUpdate() {
|
||||
this.pollChoices = this.$refs.poll.get().choices;
|
||||
const got = this.$refs.poll.get();
|
||||
this.pollChoices = got.choices;
|
||||
this.pollMultiple = got.multiple;
|
||||
this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter];
|
||||
this.saveDraft();
|
||||
},
|
||||
|
||||
|
@ -27,6 +27,7 @@
|
||||
<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
</select>
|
||||
|
@ -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>
|
@ -172,7 +172,11 @@ export default class MiOS extends EventEmitter {
|
||||
callback();
|
||||
|
||||
// Init service worker
|
||||
if (this.shouldRegisterSw) this.registerSw();
|
||||
if (this.shouldRegisterSw) {
|
||||
this.getMeta().then(data => {
|
||||
this.registerSw(data.swPublickey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// キャッシュがあったとき
|
||||
@ -302,7 +306,7 @@ export default class MiOS extends EventEmitter {
|
||||
* Register service worker
|
||||
*/
|
||||
@autobind
|
||||
private registerSw() {
|
||||
private registerSw(swPublickey) {
|
||||
// Check whether service worker and push manager supported
|
||||
const isSwSupported =
|
||||
('serviceWorker' in navigator) && ('PushManager' in window);
|
||||
@ -328,7 +332,7 @@ export default class MiOS extends EventEmitter {
|
||||
|
||||
// A public key your push server will use to send
|
||||
// messages to client apps via a push server.
|
||||
applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey)
|
||||
applicationServerKey: urlBase64ToUint8Array(swPublickey)
|
||||
};
|
||||
|
||||
// Subscribe push notification
|
||||
|
@ -26,7 +26,7 @@ import MkUserLists from './views/pages/user-lists.vue';
|
||||
import MkUserList from './views/pages/user-list.vue';
|
||||
import MkReversi from './views/pages/games/reversi.vue';
|
||||
import MkTag from './views/pages/tag.vue';
|
||||
import MkShare from './views/pages/share.vue';
|
||||
import MkShare from '../common/views/pages/share.vue';
|
||||
import MkFollow from '../common/views/pages/follow.vue';
|
||||
import MkNotFound from '../common/views/pages/not-found.vue';
|
||||
|
||||
|
@ -135,9 +135,9 @@ export default Vue.extend({
|
||||
methods: {
|
||||
fetchReplies() {
|
||||
if (this.compact) return;
|
||||
this.$root.api('notes/replies', {
|
||||
this.$root.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
|
@ -115,9 +115,9 @@ export default Vue.extend({
|
||||
|
||||
created() {
|
||||
if (this.detail) {
|
||||
this.$root.api('notes/replies', {
|
||||
this.$root.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
|
@ -105,6 +105,7 @@ export default Vue.extend({
|
||||
files: [],
|
||||
poll: false,
|
||||
pollChoices: [],
|
||||
pollMultiple: false,
|
||||
geo: null,
|
||||
visibility: 'public',
|
||||
visibleUsers: [],
|
||||
@ -273,7 +274,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onPollUpdate() {
|
||||
this.pollChoices = this.$refs.poll.get().choices;
|
||||
const got = this.$refs.poll.get();
|
||||
this.pollChoices = got.choices;
|
||||
this.pollMultiple = got.multiple;
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
|
@ -19,6 +19,7 @@
|
||||
<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
|
||||
<option value="version">{{ $t('@.widgets.version') }}</option>
|
||||
<option value="server">{{ $t('@.widgets.server') }}</option>
|
||||
<option value="queue">{{ $t('@.widgets.queue') }}</option>
|
||||
<option value="memo">{{ $t('@.widgets.memo') }}</option>
|
||||
<option value="nav">{{ $t('@.widgets.nav') }}</option>
|
||||
<option value="tips">{{ $t('@.widgets.tips') }}</option>
|
||||
|
@ -43,11 +43,11 @@ export const builtinThemes = [
|
||||
];
|
||||
|
||||
export function applyTheme(theme: Theme, persisted = true) {
|
||||
document.documentElement.classList.add('change-theme');
|
||||
document.documentElement.classList.add('changing-theme');
|
||||
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('change-theme');
|
||||
}, 500);
|
||||
document.documentElement.classList.remove('changing-theme');
|
||||
}, 1000);
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
|
@ -43,6 +43,11 @@
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"url_template": "share?text=【{title}】%0A{text}%0A{url}"
|
||||
"action": "/share/",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,11 +20,9 @@ html, body
|
||||
text-size-adjust 100%
|
||||
font-family sans-serif
|
||||
|
||||
html.change-theme
|
||||
html.changing-theme
|
||||
&, *
|
||||
transition-property all
|
||||
transition-duration 0.5s
|
||||
transition-timing-function ease
|
||||
transition background 1s ease !important
|
||||
|
||||
a
|
||||
text-decoration none
|
||||
|
@ -19,6 +19,8 @@ export type Source = {
|
||||
host: string;
|
||||
port: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
elasticsearch: {
|
||||
host: string;
|
||||
|
61
src/daemons/queue-stats.ts
Normal file
61
src/daemons/queue-stats.ts
Normal 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);
|
||||
}
|
@ -5,6 +5,8 @@ export default config.redis ? redis.createClient(
|
||||
config.redis.port,
|
||||
config.redis.host,
|
||||
{
|
||||
auth_pass: config.redis.pass
|
||||
auth_pass: config.redis.pass,
|
||||
prefix: config.redis.prefix,
|
||||
db: config.redis.db || 0
|
||||
}
|
||||
) : null;
|
||||
|
18
src/index.ts
18
src/index.ts
@ -16,6 +16,7 @@ import Xev from 'xev';
|
||||
import Logger from './services/logger';
|
||||
import serverStats from './daemons/server-stats';
|
||||
import notesStats from './daemons/notes-stats';
|
||||
import queueStats from './daemons/queue-stats';
|
||||
import loadConfig from './config/load';
|
||||
import { Config } from './config/types';
|
||||
import { lessThan } from './prelude/array';
|
||||
@ -50,6 +51,7 @@ function main() {
|
||||
if (program.daemons) {
|
||||
serverStats();
|
||||
notesStats();
|
||||
queueStats();
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +75,7 @@ function greet() {
|
||||
console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
|
||||
|
||||
console.log('');
|
||||
console.log(chalk`<${os.hostname()} {gray (PID: ${process.pid.toString()})}>`);
|
||||
console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
|
||||
}
|
||||
|
||||
bootLogger.info('Welcome to Misskey!');
|
||||
@ -117,9 +119,6 @@ async function masterMain() {
|
||||
await spawnWorkers(config.clusterLimit);
|
||||
}
|
||||
|
||||
// start queue
|
||||
require('./queue').default();
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
}
|
||||
|
||||
@ -130,6 +129,9 @@ async function workerMain() {
|
||||
// start server
|
||||
await require('./server').default();
|
||||
|
||||
// start job queue
|
||||
require('./queue').default();
|
||||
|
||||
if (cluster.isWorker) {
|
||||
// Send a 'ready' message to parent process
|
||||
process.send('ready');
|
||||
@ -150,13 +152,9 @@ async function queueMain() {
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
// start processor
|
||||
const queue = require('./queue').default();
|
||||
require('./queue').default();
|
||||
|
||||
if (queue) {
|
||||
bootLogger.succ('Queue started', null, true);
|
||||
} else {
|
||||
bootLogger.error('Queue not available');
|
||||
}
|
||||
bootLogger.succ('Queue started', null, true);
|
||||
}
|
||||
|
||||
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
|
||||
|
@ -142,7 +142,7 @@ export const mfmLanguage = P.createLanguage({
|
||||
},
|
||||
hashtag: () => P((input, i) => {
|
||||
const text = input.substr(i);
|
||||
const match = text.match(/^#([^\s\.,!\?'"#:\/]+)/i);
|
||||
const match = text.match(/^#([^\s\.,!\?'"#:\/\[\]]+)/i);
|
||||
if (!match) return P.makeFailure(i, 'not a hashtag');
|
||||
let hashtag = match[1];
|
||||
hashtag = removeOrphanedBrackets(hashtag);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as fs from 'fs';
|
||||
import * as isSvg from 'is-svg';
|
||||
import isSvg from 'is-svg';
|
||||
|
||||
export default function(path: string) {
|
||||
try {
|
||||
|
21
src/misc/convert-host.ts
Normal file
21
src/misc/convert-host.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import config from '../config';
|
||||
import { toUnicode, toASCII } from 'punycode';
|
||||
|
||||
export function getFullApAccount(username: string, host: string) {
|
||||
return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`;
|
||||
}
|
||||
|
||||
export function isSelfHost(host: string) {
|
||||
if (host == null) return true;
|
||||
return toApHost(config.host) === toApHost(host);
|
||||
}
|
||||
|
||||
export function toDbHost(host: string) {
|
||||
if (host == null) return null;
|
||||
return toUnicode(host.toLowerCase());
|
||||
}
|
||||
|
||||
export function toApHost(host: string) {
|
||||
if (host == null) return null;
|
||||
return toASCII(host.toLowerCase());
|
||||
}
|
79
src/misc/download-text-file.ts
Normal file
79
src/misc/download-text-file.ts
Normal 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;
|
||||
}
|
@ -4,6 +4,7 @@ import AccessToken from './access-token';
|
||||
import db from '../db/mongodb';
|
||||
import isObjectId from '../misc/is-objectid';
|
||||
import config from '../config';
|
||||
import { dbLogger } from '../db/logger';
|
||||
|
||||
const App = db.get<IApp>('apps');
|
||||
App.createIndex('secret');
|
||||
@ -66,6 +67,12 @@ export const pack = (
|
||||
}
|
||||
}
|
||||
|
||||
// (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
|
||||
if (_app == null) {
|
||||
dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_app.id = _app._id;
|
||||
delete _app._id;
|
||||
|
@ -3,6 +3,8 @@ import db from '../db/mongodb';
|
||||
|
||||
const Log = db.get<ILog>('logs');
|
||||
Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
|
||||
Log.createIndex('level');
|
||||
Log.createIndex('domain');
|
||||
export default Log;
|
||||
|
||||
export interface ILog {
|
||||
|
@ -19,6 +19,7 @@ Note.createIndex('userId');
|
||||
Note.createIndex('mentions');
|
||||
Note.createIndex('visibleUserIds');
|
||||
Note.createIndex('replyId');
|
||||
Note.createIndex('renoteId');
|
||||
Note.createIndex('tagsLower');
|
||||
Note.createIndex('_user.host');
|
||||
Note.createIndex('_files._id');
|
||||
@ -35,6 +36,7 @@ export type INote = {
|
||||
_id: mongo.ObjectID;
|
||||
createdAt: Date;
|
||||
deletedAt: Date;
|
||||
updatedAt?: Date;
|
||||
fileIds: mongo.ObjectID[];
|
||||
replyId: mongo.ObjectID;
|
||||
renoteId: mongo.ObjectID;
|
||||
@ -99,7 +101,9 @@ export type INote = {
|
||||
};
|
||||
|
||||
export type IPoll = {
|
||||
choices: IChoice[]
|
||||
choices: IChoice[];
|
||||
multiple?: boolean;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
|
||||
export type IChoice = {
|
||||
@ -313,15 +317,31 @@ export const pack = async (
|
||||
// Poll
|
||||
if (meId && _note.poll) {
|
||||
_note.poll = (async poll => {
|
||||
if (poll.multiple) {
|
||||
const votes = await PollVote.find({
|
||||
userId: meId,
|
||||
noteId: id
|
||||
});
|
||||
|
||||
const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
|
||||
for (const myChoice of myChoices) {
|
||||
(myChoice as any).isVoted = true;
|
||||
}
|
||||
|
||||
return poll;
|
||||
} else {
|
||||
poll.multiple = false;
|
||||
}
|
||||
|
||||
const vote = await PollVote
|
||||
.findOne({
|
||||
userId: meId,
|
||||
noteId: id
|
||||
});
|
||||
|
||||
if (vote != null) {
|
||||
const myChoice = poll.choices
|
||||
.filter((c: any) => c.id == vote.choice)[0];
|
||||
if (vote) {
|
||||
const myChoice = (poll.choices as IChoice[])
|
||||
.filter(x => x.id == vote.choice)[0] as any;
|
||||
|
||||
myChoice.isVoted = true;
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ import * as mongo from 'mongodb';
|
||||
import db from '../db/mongodb';
|
||||
|
||||
const PollVote = db.get<IPollVote>('pollVotes');
|
||||
PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
|
||||
PollVote.createIndex('userId');
|
||||
PollVote.createIndex('noteId');
|
||||
PollVote.createIndex(['userId', 'noteId'], { unique: true });
|
||||
PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
|
||||
export default PollVote;
|
||||
|
||||
export interface IPollVote {
|
||||
|
@ -1,164 +1,187 @@
|
||||
import * as Queue from 'bee-queue';
|
||||
import * as Queue from 'bull';
|
||||
import * as httpSignature from 'http-signature';
|
||||
|
||||
import config from '../config';
|
||||
import { ILocalUser } from '../models/user';
|
||||
import { program } from '../argv';
|
||||
import handler from './processors';
|
||||
|
||||
import processDeliver from './processors/deliver';
|
||||
import processInbox from './processors/inbox';
|
||||
import processDb from './processors/db';
|
||||
import { queueLogger } from './logger';
|
||||
import { IDriveFile } from '../models/drive-file';
|
||||
|
||||
const enableQueue = !program.disableQueue;
|
||||
const enableQueueProcessing = !program.onlyServer && enableQueue;
|
||||
const queueAvailable = config.redis != null;
|
||||
|
||||
const queue = initializeQueue();
|
||||
|
||||
function initializeQueue() {
|
||||
if (queueAvailable && enableQueue) {
|
||||
return new Queue('misskey-queue', {
|
||||
redis: {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
password: config.redis.pass
|
||||
},
|
||||
|
||||
removeOnSuccess: true,
|
||||
removeOnFailure: true,
|
||||
getEvents: false,
|
||||
sendEvents: false,
|
||||
storeJobs: false
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
function initializeQueue(name: string) {
|
||||
return new Queue(name, config.redis != null ? {
|
||||
redis: {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
password: config.redis.pass,
|
||||
db: config.redis.db || 0,
|
||||
},
|
||||
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue'
|
||||
} : null);
|
||||
}
|
||||
|
||||
export const deliverQueue = initializeQueue('deliver');
|
||||
export const inboxQueue = initializeQueue('inbox');
|
||||
export const dbQueue = initializeQueue('db');
|
||||
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
|
||||
deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => deliverLogger.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) {
|
||||
if (content == null) return;
|
||||
if (content == null) return null;
|
||||
|
||||
const data = {
|
||||
type: 'deliver',
|
||||
user,
|
||||
content,
|
||||
to
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data)
|
||||
.retries(8)
|
||||
.backoff('exponential', 1000)
|
||||
.save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
return deliverQueue.add(data, {
|
||||
attempts: 8,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 60 * 1000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function processInbox(activity: any, signature: httpSignature.IParsedSignature) {
|
||||
export function inbox(activity: any, signature: httpSignature.IParsedSignature) {
|
||||
const data = {
|
||||
type: 'processInbox',
|
||||
activity: activity,
|
||||
signature
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data)
|
||||
.retries(3)
|
||||
.backoff('exponential', 500)
|
||||
.save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
return inboxQueue.add(data, {
|
||||
attempts: 8,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createDeleteNotesJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'deleteNotes',
|
||||
return dbQueue.add('deleteNotes', {
|
||||
user: user
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createDeleteDriveFilesJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'deleteDriveFiles',
|
||||
return dbQueue.add('deleteDriveFiles', {
|
||||
user: user
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportNotesJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'exportNotes',
|
||||
return dbQueue.add('exportNotes', {
|
||||
user: user
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportFollowingJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'exportFollowing',
|
||||
return dbQueue.add('exportFollowing', {
|
||||
user: user
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportMuteJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'exportMute',
|
||||
return dbQueue.add('exportMute', {
|
||||
user: user
|
||||
};
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportBlockingJob(user: ILocalUser) {
|
||||
const data = {
|
||||
type: 'exportBlocking',
|
||||
return dbQueue.add('exportBlocking', {
|
||||
user: user
|
||||
};
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
return queue.createJob(data).save();
|
||||
} else {
|
||||
return handler({ data }, () => {});
|
||||
}
|
||||
export function createExportUserListsJob(user: ILocalUser) {
|
||||
return dbQueue.add('exportUserLists', {
|
||||
user: user
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
|
||||
return dbQueue.add('importFollowing', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
|
||||
return dbQueue.add('importUserLists', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (queueAvailable && enableQueueProcessing) {
|
||||
queue.process(128, handler);
|
||||
queueLogger.succ('Processing started');
|
||||
if (!program.onlyServer) {
|
||||
deliverQueue.process(128, processDeliver);
|
||||
inboxQueue.process(128, processInbox);
|
||||
processDb(dbQueue);
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
queue.destroy().then(n => {
|
||||
queueLogger.succ(`All job removed (${n} jobs)`);
|
||||
deliverQueue.once('cleaned', (jobs, status) => {
|
||||
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
deliverQueue.clean(0, 'wait');
|
||||
|
||||
inboxQueue.once('cleaned', (jobs, status) => {
|
||||
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
inboxQueue.clean(0, 'wait');
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import User from '../../models/user';
|
||||
import DriveFile from '../../models/drive-file';
|
||||
import deleteFile from '../../services/drive/delete-file';
|
||||
import { queueLogger } from '../../logger';
|
||||
import User from '../../../models/user';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import deleteFile from '../../../services/drive/delete-file';
|
||||
|
||||
const logger = queueLogger.createSubLogger('delete-drive-files');
|
||||
|
||||
export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
|
||||
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Deleting drive files of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -32,7 +32,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (files.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(deletedCount / total);
|
||||
job.progress(deletedCount / total);
|
||||
}
|
||||
|
||||
logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);
|
@ -1,14 +1,14 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import Note from '../../models/note';
|
||||
import deleteNote from '../../services/note/delete';
|
||||
import User from '../../models/user';
|
||||
import { queueLogger } from '../../logger';
|
||||
import Note from '../../../models/note';
|
||||
import deleteNote from '../../../services/note/delete';
|
||||
import User from '../../../models/user';
|
||||
|
||||
const logger = queueLogger.createSubLogger('delete-notes');
|
||||
|
||||
export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
|
||||
export async function deleteNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Deleting notes of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -32,7 +32,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (notes.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(deletedCount / total);
|
||||
job.progress(deletedCount / total);
|
||||
}
|
||||
|
||||
logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);
|
@ -1,18 +1,18 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import addFile from '../../services/drive/add-file';
|
||||
import User from '../../models/user';
|
||||
import { queueLogger } from '../../logger';
|
||||
import addFile from '../../../services/drive/add-file';
|
||||
import User from '../../../models/user';
|
||||
import dateFormat = require('dateformat');
|
||||
import Blocking from '../../models/blocking';
|
||||
import config from '../../config';
|
||||
import Blocking from '../../../models/blocking';
|
||||
import { getFullApAccount } from '../../../misc/convert-host';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-blocking');
|
||||
|
||||
export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
|
||||
export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Exporting blocking of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -48,7 +48,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (blockings.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
for (const block of blockings) {
|
||||
const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
|
||||
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
@ -74,7 +74,7 @@ export async function exportBlocking(job: bq.Job, done: any): Promise<void> {
|
||||
blockerId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(exportedCount / total);
|
||||
job.progress(exportedCount / total);
|
||||
}
|
||||
|
||||
stream.end();
|
@ -1,18 +1,18 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import addFile from '../../services/drive/add-file';
|
||||
import User from '../../models/user';
|
||||
import { queueLogger } from '../../logger';
|
||||
import addFile from '../../../services/drive/add-file';
|
||||
import User from '../../../models/user';
|
||||
import dateFormat = require('dateformat');
|
||||
import Following from '../../models/following';
|
||||
import config from '../../config';
|
||||
import Following from '../../../models/following';
|
||||
import { getFullApAccount } from '../../../misc/convert-host';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-following');
|
||||
|
||||
export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
|
||||
export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Exporting following of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -48,7 +48,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (followings.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
for (const following of followings) {
|
||||
const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
|
||||
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
@ -74,7 +74,7 @@ export async function exportFollowing(job: bq.Job, done: any): Promise<void> {
|
||||
followerId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(exportedCount / total);
|
||||
job.progress(exportedCount / total);
|
||||
}
|
||||
|
||||
stream.end();
|
@ -1,18 +1,18 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import addFile from '../../services/drive/add-file';
|
||||
import User from '../../models/user';
|
||||
import { queueLogger } from '../../logger';
|
||||
import addFile from '../../../services/drive/add-file';
|
||||
import User from '../../../models/user';
|
||||
import dateFormat = require('dateformat');
|
||||
import Mute from '../../models/mute';
|
||||
import config from '../../config';
|
||||
import Mute from '../../../models/mute';
|
||||
import { getFullApAccount } from '../../../misc/convert-host';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-mute');
|
||||
|
||||
export async function exportMute(job: bq.Job, done: any): Promise<void> {
|
||||
export async function exportMute(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Exporting mute of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -48,7 +48,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (mutes.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
for (const mute of mutes) {
|
||||
const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
|
||||
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`;
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
@ -74,7 +74,7 @@ export async function exportMute(job: bq.Job, done: any): Promise<void> {
|
||||
muterId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(exportedCount / total);
|
||||
job.progress(exportedCount / total);
|
||||
}
|
||||
|
||||
stream.end();
|
@ -1,17 +1,17 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../logger';
|
||||
import Note, { INote } from '../../models/note';
|
||||
import addFile from '../../services/drive/add-file';
|
||||
import User from '../../models/user';
|
||||
import { queueLogger } from '../../logger';
|
||||
import Note, { INote } from '../../../models/note';
|
||||
import addFile from '../../../services/drive/add-file';
|
||||
import User from '../../../models/user';
|
||||
import dateFormat = require('dateformat');
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-notes');
|
||||
|
||||
export async function exportNotes(job: bq.Job, done: any): Promise<void> {
|
||||
export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Exporting notes of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
@ -58,7 +58,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
|
||||
|
||||
if (notes.length === 0) {
|
||||
ended = true;
|
||||
if (job.reportProgress) job.reportProgress(100);
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export async function exportNotes(job: bq.Job, done: any): Promise<void> {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
if (job.reportProgress) job.reportProgress(exportedNotesCount / total);
|
||||
job.progress(exportedNotesCount / total);
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
73
src/queue/processors/db/export-user-lists.ts
Normal file
73
src/queue/processors/db/export-user-lists.ts
Normal 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 UserList from '../../../models/user-list';
|
||||
import { getFullApAccount } from '../../../misc/convert-host';
|
||||
|
||||
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 = getFullApAccount(u.username, u.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();
|
||||
}
|
55
src/queue/processors/db/import-following.ts
Normal file
55
src/queue/processors/db/import-following.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import User from '../../../models/user';
|
||||
import follow from '../../../services/following/create';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import resolveUser from '../../../remote/resolve-user';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-following');
|
||||
|
||||
export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Importing following of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||
});
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(job.data.fileId.toString())
|
||||
});
|
||||
|
||||
const url = getOriginalUrl(file);
|
||||
|
||||
const csv = await downloadTextFile(url);
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
const { username, host } = parseAcct(line.trim());
|
||||
|
||||
let target = isSelfHost(host) ? await User.findOne({
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await User.findOne({
|
||||
host: toDbHost(host),
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await resolveUser(username, host);
|
||||
}
|
||||
|
||||
logger.info(`Follow ${target._id} ...`);
|
||||
|
||||
follow(user, target);
|
||||
}
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
70
src/queue/processors/db/import-user-lists.ts
Normal file
70
src/queue/processors/db/import-user-lists.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import * as Bull from 'bull';
|
||||
import * as mongo from 'mongodb';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import User from '../../../models/user';
|
||||
import UserList from '../../../models/user-list';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import { getOriginalUrl } from '../../../misc/get-drive-file-url';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import resolveUser from '../../../remote/resolve-user';
|
||||
import { pushUserToUserList } from '../../../services/user-list/push';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-user-lists');
|
||||
|
||||
export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Importing user lists of ${job.data.user._id} ...`);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||
});
|
||||
|
||||
const file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(job.data.fileId.toString())
|
||||
});
|
||||
|
||||
const url = getOriginalUrl(file);
|
||||
|
||||
const csv = await downloadTextFile(url);
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
const listName = line.split(',')[0].trim();
|
||||
const { username, host } = parseAcct(line.split(',')[1].trim());
|
||||
|
||||
let list = await UserList.findOne({
|
||||
userId: user._id,
|
||||
title: listName
|
||||
});
|
||||
|
||||
if (list == null) {
|
||||
list = await UserList.insert({
|
||||
createdAt: new Date(),
|
||||
userId: user._id,
|
||||
title: listName,
|
||||
userIds: []
|
||||
});
|
||||
}
|
||||
|
||||
let target = isSelfHost(host) ? await User.findOne({
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await User.findOne({
|
||||
host: toDbHost(host),
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
if (host == null && target == null) continue;
|
||||
if (list.userIds.some(id => id.equals(target._id))) continue;
|
||||
|
||||
if (target == null) {
|
||||
target = await resolveUser(username, host);
|
||||
}
|
||||
|
||||
pushUserToUserList(target, list);
|
||||
}
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
@ -1,31 +1,28 @@
|
||||
import deliver from './http/deliver';
|
||||
import processInbox from './http/process-inbox';
|
||||
import * as Bull from 'bull';
|
||||
import { deleteNotes } from './delete-notes';
|
||||
import { deleteDriveFiles } from './delete-drive-files';
|
||||
import { exportNotes } from './export-notes';
|
||||
import { exportFollowing } from './export-following';
|
||||
import { exportMute } from './export-mute';
|
||||
import { exportBlocking } from './export-blocking';
|
||||
import { queueLogger } from '../logger';
|
||||
import { exportUserLists } from './export-user-lists';
|
||||
import { importFollowing } from './import-following';
|
||||
import { importUserLists } from './import-user-lists';
|
||||
|
||||
const handlers: any = {
|
||||
deliver,
|
||||
processInbox,
|
||||
const jobs = {
|
||||
deleteNotes,
|
||||
deleteDriveFiles,
|
||||
exportNotes,
|
||||
exportFollowing,
|
||||
exportMute,
|
||||
exportBlocking,
|
||||
};
|
||||
exportUserLists,
|
||||
importFollowing,
|
||||
importUserLists
|
||||
} as any;
|
||||
|
||||
export default (job: any, done: any) => {
|
||||
const handler = handlers[job.data.type];
|
||||
|
||||
if (handler) {
|
||||
handler(job, done);
|
||||
} else {
|
||||
queueLogger.error(`Unknown job: ${job.data.type}`);
|
||||
done();
|
||||
export default function(dbQueue: Bull.Queue) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
dbQueue.process(k, v as any);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,15 +1,22 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import request from '../../remote/activitypub/request';
|
||||
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
import Logger from '../../services/logger';
|
||||
|
||||
import request from '../../../remote/activitypub/request';
|
||||
import { queueLogger } from '../../logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../../models/instance';
|
||||
import instanceChart from '../../../services/chart/instance';
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
let latest: string = null;
|
||||
|
||||
export default async (job: Bull.Job) => {
|
||||
const { host } = new URL(job.data.to);
|
||||
|
||||
try {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
logger.debug(`delivering ${latest}`);
|
||||
}
|
||||
|
||||
await request(job.data.user, job.data.to, job.data.content);
|
||||
|
||||
// Update stats
|
||||
@ -26,7 +33,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
instanceChart.requestSent(i.host, true);
|
||||
});
|
||||
|
||||
done();
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
// Update stats
|
||||
registerOrFetchInstanceDoc(host).then(i => {
|
||||
@ -42,18 +49,21 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
});
|
||||
|
||||
if (res != null && res.hasOwnProperty('statusCode')) {
|
||||
queueLogger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
|
||||
logger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
|
||||
|
||||
// 4xx
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||
done();
|
||||
} else {
|
||||
done(res.statusMessage);
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
queueLogger.warn(`deliver failed: ${res} to=${job.data.to}`);
|
||||
done();
|
||||
// DNS error, socket error, timeout ...
|
||||
logger.warn(`deliver failed: ${res} to=${job.data.to}`);
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,21 +1,21 @@
|
||||
import * as bq from 'bee-queue';
|
||||
import * as Bull from 'bull';
|
||||
import * as httpSignature from 'http-signature';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import User, { IRemoteUser } from '../../../models/user';
|
||||
import perform from '../../../remote/activitypub/perform';
|
||||
import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person';
|
||||
import parseAcct from '../../misc/acct/parse';
|
||||
import User, { IRemoteUser } from '../../models/user';
|
||||
import perform from '../../remote/activitypub/perform';
|
||||
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { URL } from 'url';
|
||||
import { publishApLogStream } from '../../../services/stream';
|
||||
import Logger from '../../../services/logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../../models/instance';
|
||||
import instanceChart from '../../../services/chart/instance';
|
||||
import { publishApLogStream } from '../../services/stream';
|
||||
import Logger from '../../services/logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
||||
export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
export default async (job: Bull.Job): Promise<void> => {
|
||||
const signature = job.data.signature;
|
||||
const activity = job.data.activity;
|
||||
|
||||
@ -33,7 +33,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
|
||||
if (host === null) {
|
||||
logger.warn(`request was made by local user: @${username}`);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -42,7 +41,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
ValidateActivity(activity, host);
|
||||
} catch (e) {
|
||||
logger.warn(e.message);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -50,8 +48,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const instance = await Instance.findOne({ host: host.toLowerCase() });
|
||||
if (instance && instance.isBlocked) {
|
||||
logger.warn(`Blocked request: ${host}`);
|
||||
done();
|
||||
logger.info(`Blocked request: ${host}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -63,7 +60,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
ValidateActivity(activity, host);
|
||||
} catch (e) {
|
||||
logger.warn(e.message);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,7 +68,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
const instance = await Instance.findOne({ host: host.toLowerCase() });
|
||||
if (instance && instance.isBlocked) {
|
||||
logger.warn(`Blocked request: ${host}`);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -82,7 +77,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
}) as IRemoteUser;
|
||||
}
|
||||
|
||||
// Update activityの場合は、ここで署名検証/更新処理まで実施して終了
|
||||
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
|
||||
if (activity.type === 'Update') {
|
||||
if (activity.object && activity.object.type === 'Person') {
|
||||
if (user == null) {
|
||||
@ -92,9 +87,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
} else {
|
||||
updatePerson(activity.actor, null, activity.object);
|
||||
}
|
||||
return;
|
||||
}
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
|
||||
@ -103,13 +97,11 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
}
|
||||
|
||||
if (user === null) {
|
||||
done(new Error('failed to resolve user'));
|
||||
return;
|
||||
throw new Error('failed to resolve user');
|
||||
}
|
||||
|
||||
if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
|
||||
logger.error('signature verification failed');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,12 +128,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
});
|
||||
|
||||
// アクティビティを処理
|
||||
try {
|
||||
await perform(user, activity);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
await perform(user, activity);
|
||||
};
|
||||
|
||||
/**
|
@ -27,6 +27,10 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
|
||||
announceNote(resolver, actor, activity, object as INote);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
announceNote(resolver, actor, activity, object as INote);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn(`Unknown announce type: ${object.type}`);
|
||||
break;
|
||||
|
@ -29,7 +29,19 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
||||
return;
|
||||
}
|
||||
|
||||
const renote = await resolveNote(note);
|
||||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await resolveNote(note);
|
||||
} catch (e) {
|
||||
// 対象が4xxならスキップ
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`);
|
||||
return;
|
||||
}
|
||||
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/user';
|
||||
import createNote from './note';
|
||||
import createImage from './image';
|
||||
import createNote from './note';
|
||||
import { ICreate } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
@ -32,6 +32,10 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
createNote(resolver, actor, object);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
createNote(resolver, actor, object);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn(`Unknown type: ${object.type}`);
|
||||
break;
|
||||
|
@ -24,6 +24,10 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
|
||||
deleteNote(actor, uri);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
deleteNote(actor, uri);
|
||||
break;
|
||||
|
||||
case 'Tombstone':
|
||||
const note = await Note.findOne({ uri });
|
||||
if (note != null) {
|
||||
|
@ -2,6 +2,7 @@ import { Object } from '../type';
|
||||
import { IRemoteUser } from '../../../models/user';
|
||||
import create from './create';
|
||||
import performDeleteActivity from './delete';
|
||||
import performUpdateActivity from './update';
|
||||
import follow from './follow';
|
||||
import undo from './undo';
|
||||
import like from './like';
|
||||
@ -23,6 +24,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
||||
await performDeleteActivity(actor, activity);
|
||||
break;
|
||||
|
||||
case 'Update':
|
||||
await performUpdateActivity(actor, activity);
|
||||
break;
|
||||
|
||||
case 'Follow':
|
||||
await follow(actor, activity);
|
||||
break;
|
||||
|
28
src/remote/activitypub/kernel/update/index.ts
Normal file
28
src/remote/activitypub/kernel/update/index.ts
Normal 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;
|
||||
}
|
||||
};
|
@ -27,7 +27,17 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
|
||||
const instance = await fetchMeta();
|
||||
const cache = instance.cacheRemoteFiles;
|
||||
|
||||
let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
|
||||
let file;
|
||||
try {
|
||||
file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
|
||||
} catch (e) {
|
||||
// 4xxの場合は添付されてなかったことにする
|
||||
if (e >= 400 && e < 500) {
|
||||
logger.warn(`Ignored image: ${image.url} - ${e}`);
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (file.metadata.isRemote) {
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
|
@ -18,6 +18,7 @@ import { extractPollFromQuestion } from './question';
|
||||
import vote from '../../../services/note/polls/vote';
|
||||
import { apLogger } from '../logger';
|
||||
import { IDriveFile } from '../../../models/drive-file';
|
||||
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -52,15 +53,23 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
|
||||
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const object = await resolver.resolve(value) as any;
|
||||
const object: any = await resolver.resolve(value);
|
||||
|
||||
if (object == null || object.type !== 'Note') {
|
||||
logger.error(`invalid note: ${object}`);
|
||||
if (!object || !['Note', 'Question'].includes(object.type)) {
|
||||
logger.error(`invalid note: ${value}`, {
|
||||
resolver: {
|
||||
history: resolver.getHistory()
|
||||
},
|
||||
value: value,
|
||||
object: object
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const note: INoteActivityStreamsObject = object;
|
||||
|
||||
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
// 投稿者をフェッチ
|
||||
@ -72,6 +81,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
}
|
||||
|
||||
//#region Visibility
|
||||
note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to;
|
||||
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
|
||||
|
||||
let visibility = 'public';
|
||||
let visibleUsers: IUser[] = [];
|
||||
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
|
||||
@ -83,7 +95,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
visibility = 'specified';
|
||||
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endergion
|
||||
|
||||
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
|
||||
@ -95,13 +107,26 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
// TODO: attachmentは必ずしも配列ではない
|
||||
// Noteがsensitiveなら添付もsensitiveにする
|
||||
const limit = promiseLimit(2);
|
||||
|
||||
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
|
||||
const files = note.attachment
|
||||
.map(attach => attach.sensitive = note.sensitive)
|
||||
? await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))
|
||||
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
|
||||
.filter(image => image != null)
|
||||
: [];
|
||||
|
||||
// リプライ
|
||||
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
||||
const reply: INote = note.inReplyTo
|
||||
? await resolveNote(note.inReplyTo, resolver).catch(e => {
|
||||
// 4xxの場合はリプライしてないことにする
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `);
|
||||
return null;
|
||||
}
|
||||
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
|
||||
throw e;
|
||||
})
|
||||
: null;
|
||||
|
||||
// 引用
|
||||
let quote: INote;
|
||||
@ -113,15 +138,34 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
const cw = note.summary === '' ? null : note.summary;
|
||||
|
||||
// テキストのパース
|
||||
const text = note._misskey_content ? note._misskey_content : fromHtml(note.content);
|
||||
const text = note._misskey_content || fromHtml(note.content);
|
||||
|
||||
// vote
|
||||
if (reply && reply.poll && text != null) {
|
||||
const m = text.match(/([0-9])$/);
|
||||
if (m) {
|
||||
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`);
|
||||
await vote(actor, reply, Number(m[1]));
|
||||
if (reply && reply.poll) {
|
||||
const tryCreateVote = async (name: string, index: number): Promise<null> => {
|
||||
if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
|
||||
logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||
} else if (index >= 0) {
|
||||
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||
await vote(actor, reply, index);
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
deliverQuestionUpdate(reply._id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (note.name) {
|
||||
return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
|
||||
}
|
||||
|
||||
// 後方互換性のため
|
||||
if (text) {
|
||||
const m = text.match(/(\d+)$/);
|
||||
|
||||
if (m) {
|
||||
return await tryCreateVote(m[0], Number(m[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +177,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
const questionUri = note._misskey_question;
|
||||
const poll = questionUri ? await extractPollFromQuestion(questionUri).catch(() => undefined) : undefined;
|
||||
const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined);
|
||||
|
||||
// ユーザーの情報が古かったらついでに更新しておく
|
||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
@ -142,11 +186,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
|
||||
return await post(actor, {
|
||||
createdAt: new Date(note.published),
|
||||
files: files,
|
||||
files,
|
||||
reply,
|
||||
renote: quote,
|
||||
cw: cw,
|
||||
text: text,
|
||||
cw,
|
||||
text,
|
||||
viaMobile: false,
|
||||
localOnly: false,
|
||||
geo: undefined,
|
||||
|
@ -1,19 +1,79 @@
|
||||
import { IChoice, IPoll } from '../../../models/note';
|
||||
import config from '../../../config';
|
||||
import Note, { IChoice, IPoll } from '../../../models/note';
|
||||
import Resolver from '../resolver';
|
||||
import { IQuestion } from '../type';
|
||||
import { apLogger } from '../logger';
|
||||
|
||||
export async function extractPollFromQuestion(questionUri: string): Promise<IPoll> {
|
||||
const resolver = new Resolver();
|
||||
const question = await resolver.resolve(questionUri) as any;
|
||||
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
|
||||
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
|
||||
const multiple = !question.oneOf;
|
||||
const expiresAt = question.endTime ? new Date(question.endTime) : null;
|
||||
|
||||
const choices: IChoice[] = question.oneOf.map((x: any, i: number) => {
|
||||
return {
|
||||
id: i,
|
||||
text: x.name,
|
||||
votes: x._misskey_votes || 0,
|
||||
} as IChoice;
|
||||
});
|
||||
if (multiple && !question.anyOf) {
|
||||
throw 'invalid question';
|
||||
}
|
||||
|
||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||
.map((x, i) => ({
|
||||
id: i,
|
||||
text: x.name,
|
||||
votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
|
||||
} as IChoice));
|
||||
|
||||
return {
|
||||
choices
|
||||
choices,
|
||||
multiple,
|
||||
expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update votes of Question
|
||||
* @param uri URI of AP Question object
|
||||
* @returns true if updated
|
||||
*/
|
||||
export async function updateQuestion(value: any) {
|
||||
const uri = typeof value == 'string' ? value : value.id;
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (uri.startsWith(config.url + '/')) throw 'uri points local';
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const note = await Note.findOne({ uri });
|
||||
|
||||
if (note == null) throw 'Question is not registed';
|
||||
//#endregion
|
||||
|
||||
// resolve new Question object
|
||||
const resolver = new Resolver();
|
||||
const question = await resolver.resolve(value) as IQuestion;
|
||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||
|
||||
if (question.type !== 'Question') throw 'object is not a Question';
|
||||
|
||||
const apChoices = question.oneOf || question.anyOf;
|
||||
const dbChoices = note.poll.choices;
|
||||
|
||||
let changed = false;
|
||||
|
||||
for (const db of dbChoices) {
|
||||
const oldCount = db.votes;
|
||||
const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems;
|
||||
|
||||
if (oldCount != newCount) {
|
||||
changed = true;
|
||||
db.votes = newCount;
|
||||
}
|
||||
}
|
||||
|
||||
await Note.update({
|
||||
_id: note._id
|
||||
}, {
|
||||
$set: {
|
||||
'poll.choices': dbChoices,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user