Compare commits

...

65 Commits

Author SHA1 Message Date
16fb7c4557 11.37.1 2020-01-07 23:35:29 +09:00
8aafafe416 Fix #5688 (#5689)
* Resolve #5688

* あああああ

* 😇

* :thinking_face:

* Update detect-mine.ts

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-01-07 23:34:17 +09:00
90cfd87f46 11.37.0 2020-01-07 22:04:46 +09:00
5ff89e1538 New Crowdin translations (#5602)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

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

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)
2020-01-07 22:04:19 +09:00
8b6968c665 Update dependencies 🚀 2020-01-07 21:51:37 +09:00
6ef1b1b1a2 カスタム絵文字リアクションの絵文字がNoteに添付されないのを修正 (#5686)
* カスタム絵文字リアクションの絵文字がNoteに添付されないのを修正

* ねんのため

* 記述順
2020-01-07 12:28:20 +09:00
fc0e1955d7 Update README.md [AUTOGEN] (#5680) 2020-01-05 20:58:42 +09:00
c3b8123e32 Stacked bar chart の修正 (#5681)
* Revert "Stacked bar chart がおかしいのを修正 (#5654)"

This reverts commit b16e5bd136.

* apexcharts@3.12.0
2020-01-05 20:12:37 +09:00
b6ec3f655a Update README.md [AUTOGEN] (#5676) 2020-01-04 22:25:31 +09:00
e37840d870 ドライブ関連の修正 (#5673)
* ✌️

* Update add-file.ts

* fix
2020-01-04 07:20:41 +09:00
85b7eb1fb8 2020 2020-01-02 04:15:16 +09:00
541f5f1314 Hide suspended user profile (#5452) 2020-01-02 02:47:20 +09:00
a3c7901f87 Fix: リモートプロキシ時にサムネイルのContent-Typeがおかしい (#5669) 2020-01-02 02:46:13 +09:00
78ef0a9929 ドライブファイルURL生成などの修正 (#5671)
* Fix: リモートプロキシ時にサムネイルのContent-Typeがおかしい

* fix drive
2020-01-02 02:45:05 +09:00
b0bb5d8dfc 期限切れ/未保存リモートファイルのローカルプロキシ (#5655)
* Media Proxy を実装

* サンプルを追加

* https://github.com/syuilo/misskey/pull/5649#discussion_r359967471 の修正

* https://github.com/syuilo/misskey/pull/5649#discussion_r359967966 の修正

* https://github.com/syuilo/misskey/pull/5649#discussion_r359968219 の修正

* 期限切れ/未保存リモートファイルのローカルプロキシ

* 設定

* 説明

* comment out

* fix

Co-authored-by: 和風ドレッシング <37681609+CookieRamen@users.noreply.github.com>
2019-12-31 17:23:47 +09:00
307fc18138 Update README.md [AUTOGEN] (#5664) 2019-12-31 02:47:33 +09:00
330f2dedf7 Update README.md [AUTOGEN] (#5661) 2019-12-27 06:43:54 +09:00
c5c074f201 Update ja-JP.yml
誤字修正 少数→小数  
数→数値のほうがよさそうなので変更
2019-12-24 19:54:53 +09:00
b16e5bd136 Stacked bar chart がおかしいのを修正 (#5654)
* Fix stacked bar chart

* Fix drive stacked bar chart
2019-12-21 16:49:01 +09:00
e13f778b33 Update README.md [AUTOGEN] (#5647) 2019-12-20 09:48:25 +09:00
953142115c Update dependencies 🚀 2019-12-20 04:46:28 +09:00
1eb5578063 Add round function 2019-12-20 02:09:51 +09:00
9bc07c1a1c Media Proxy を実装 (#5649)
* Media Proxy を実装

* サンプルを追加
2019-12-20 01:54:28 +09:00
cbbdc98744 /files/ 下のヘッダ設定タイミングを修正 (#5650) 2019-12-20 01:39:59 +09:00
4229065a69 Update README.md [AUTOGEN] (#5643) 2019-12-17 04:43:31 +09:00
932436096f 管理画面でstatsを継続リクエストしないように (#5608) 2019-12-15 03:43:31 +09:00
d95242cab0 ミュート/ブロックでページングと解除ができるように (#5610) 2019-12-15 03:42:33 +09:00
4214a0618e Update showdown to 1.9.1 (#5615) 2019-12-15 03:41:18 +09:00
c012f4f880 AP引用でquoteUrlに対応 (#5632)
* Supports quoteUrl

* Quote resolveをリトライする

* Update src/remote/activitypub/models/note.ts

Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update src/remote/activitypub/models/note.ts

Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update src/remote/activitypub/models/note.ts

Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update src/remote/activitypub/models/note.ts

Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2019-12-15 03:37:54 +09:00
3e85aad80a Implement Talk has read federation (#5636)
* Talk read

* fix

* 複数のRead ActivityはCollectionとして送るように

* あ
2019-12-15 03:37:19 +09:00
648be3005f Fix #5637 (#5638) 2019-12-15 03:35:09 +09:00
66165b1935 Redis prefixにホスト名を使用するように (Resolve #5639) (#5640) 2019-12-15 03:34:11 +09:00
e9360ac892 Fix AP inbox Announce (#5641) 2019-12-15 03:32:48 +09:00
1d234e10bd Fix #4800 (#5622) 2019-12-12 00:49:30 +09:00
2a9de356db Fix #5611 (#5612) 2019-12-12 00:46:10 +09:00
43f3f8a058 Resolve syuilo#5548 (#5607) 2019-12-12 00:41:26 +09:00
4998ba8866 Fix #5424 (#5604) 2019-12-12 00:39:59 +09:00
d18291cf0c gulpのminifyプロセスの改善 (#5624)
* Use terser instead of uglify

* Use gulp-clean-css instead of gulp-cssnano

* isProduction分岐を削除
2019-12-12 00:37:58 +09:00
fe9371f06c AP Signatureヘッダの特殊処理を削除 (#5628) 2019-12-11 23:14:51 +09:00
05a15afadb Update README.md [AUTOGEN] (#5634) 2019-12-11 21:57:49 +09:00
93417912bb Update README.md [AUTOGEN] (#5631) 2019-12-10 21:40:42 +09:00
07a565a61a Update README.md [AUTOGEN] (#5626) 2019-12-10 21:25:36 +09:00
332b13dfd0 Update README.md [AUTOGEN] (#5618) 2019-12-03 08:53:08 +09:00
81477ea7ee Update README.md [AUTOGEN] (#5613) 2019-12-02 15:43:06 +09:00
39e84539cd Update README.md [AUTOGEN] (#5606) 2019-12-01 17:39:30 +09:00
6496fbf923 Update README.md [AUTOGEN] (#5601) 2019-11-25 18:27:00 +09:00
de57dd7c97 11.36.0 2019-11-24 17:12:05 +09:00
9985c010bc Update master.ts 2019-11-24 17:11:53 +09:00
f7a328d66e Update dependencies 🚀 2019-11-24 17:09:32 +09:00
50598bcefb New Crowdin translations (#5573)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Polish)
2019-11-24 17:01:34 +09:00
9c38e9722a Fix bug 2019-11-24 16:43:19 +09:00
b241967fb9 Fix: ローカルにフォロワー限定投稿が流れてくる (#5598) 2019-11-24 08:31:57 +09:00
1f86a6d329 Fix bug 2019-11-23 09:43:47 +09:00
7a6b5b0bfc Remove unused import 2019-11-18 06:30:58 +09:00
e406791b7b Fix bug 2019-11-18 06:27:22 +09:00
4ce2f596ee Refactor 2019-11-18 06:25:47 +09:00
567f71fe61 Refactor 2019-11-18 06:23:44 +09:00
70bb5879f9 boot: remove setAttribute() calls and translate reload msg (#5532)
* boot: remove setAttribute() calls and translate reload msg

* Update src/client/app/boot.js

Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2019-11-17 00:44:21 +09:00
cfd2d84b14 Link joinmisskey 2019-11-17 00:15:48 +09:00
44ab428803 無駄なAP deliverをしないように (#5589)
* DeliverManager, note/create

* recipe

* followers delivers

* comment

* rename

* fix

* cleanup
2019-11-09 18:51:54 +09:00
b34b728fbb Resolve #5587 (#5588)
* Resolve #5587

* stat
2019-11-09 18:24:41 +09:00
8ada1725bf 管理画面のジョブキュー一覧の修正 (#5586)
* Fix: inboxのジョブキューが表示されない

* ジョブキューで試行回数等を表示するように

* DBとオブジェクトストレージのジョブキューが表示されるように
2019-11-07 05:41:44 +09:00
873444c3c6 APの統計とログの修正と強化 (#5585)
* Fix #5580

* Improve AP logging
2019-11-07 00:02:18 +09:00
8bdd4fd061 Resolve #5582 (#5583) 2019-11-06 23:56:56 +09:00
f3b518fb62 Update yarn.lock (#5584) 2019-11-06 23:56:24 +09:00
112 changed files with 2498 additions and 2166 deletions

View File

@ -140,3 +140,6 @@ autoAdmin: true
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT #proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 #proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 #proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy

View File

@ -1,6 +1,59 @@
ChangeLog ChangeLog
========= =========
11.37.1 (2020/01/07)
--------------------
### 🐛Fixes
* ファイルがアップロードできない問題を修正
11.37.0 (2020/01/07)
--------------------
### ✨Improvements
* AP引用でquoteUrlに対応
* トークの既読を連合
* 期限切れ/未保存リモートファイルのローカルプロキシ機能
* ミュート/ブロックでページングと解除ができるように
* Redis prefixにホスト名を使用するように
* AP Resolverの長いエラーメッセージをトリムするように
* 管理画面でstatsを継続リクエストしないように
* 凍結ユーザーのプロファイルなどを表示しないように
* クライアントで、thumbanilUrlが提供されていない画像はプレビューしないように
* svgでも画像の平均色を計算するように
* 画像の平均色を計算するとき、透過部分のある画像では一律で背景を#fff(白)に
* Pages: 小数点を丸める関数を追加
### 🐛Fixes
* カスタム絵文字リアクションの絵文字がNoteに添付されないのを修正
* リモートプロキシ時にサムネイルのContent-Typeがおかしい問題を修正
* /files/ 下のヘッダ設定タイミングを修正
* イベントが同じRedisを使用する他のMisskeyインスタンスに飛んでしまう問題を修正
* AP inbox Announce の処理の修正
* followers, direct投稿の存在がユーザーの投稿一覧に表示されている問題を修正
* Stacked bar chart がおかしいのを修正
* リストのインポートがエラーが出るとそこで終わってしまう問題を修正
* サムネイル/webpublicのファイル形式がjpeg/pngに固定されていたのをファイルを基に送出するように
* syslogが使えない問題を修正
11.36.0 (2019/11/24)
--------------------
### ✨Improvements
* ジョブキューで試行回数等を表示するように
* deliverエラー等の同じようなログが複数出てこないように、上でまとめて出すように
* deliverエラーのログではキューの詳細情報を格納しないように
* inbox/deliverのログに試行回数とキューが作られてからの時間を表示するように
* 無駄なAP deliverをしないように
* boot: remove setAttribute() calls and translate reload msg
### 🐛Fixes
* メンションの通知が届かない可能性がある問題を修正
* ブロックや閉鎖済みインスタンスのステータスが更新されてしまう問題を修正
* 「フォロワーを解除」アクティビティを正しく受け取らない問題を修正
* inboxのジョブキューが表示されない問題を修正
* ローカルにフォロワー限定投稿が流れてくる問題を修正
* リモートユーザーのアイコンがサムネイルで表示されない問題を修正
* DBとオブジェクトストレージのジョブキューが表示されない問題を修正
* エラーが発生したときにサーバーがクラッシュすることがある問題を修正
11.35.1 (2019/11/05) 11.35.1 (2019/11/05)
-------------------- --------------------
### 🐛Fixes ### 🐛Fixes

View File

@ -1,6 +1,6 @@
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a> <a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
[![Misskey](/assets/title.png)](https://misskey.io/) [![Misskey](/assets/title.png)](https://join.misskey.page/)
================================================================ ================================================================
[![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey) [![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey)
@ -10,7 +10,7 @@
**A forever evolving, sophisticated microblogging platform.** **A forever evolving, sophisticated microblogging platform.**
<p align="justify"> <p align="justify">
<a href="https://misskey.io">Misskey</a> is a decentralized microblogging platform born on Earth. <a href="https://join.misskey.page/">Misskey</a> is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized), Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms. it is mutually linked with other social media platforms.
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://join.misskey.page/">Find an instance!</a> Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://join.misskey.page/">Find an instance!</a>
@ -103,76 +103,88 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
---------------------------------------------------------------- ----------------------------------------------------------------
<!-- PATREON_START --> <!-- PATREON_START -->
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20010324/b8af4bd31ae34fbf8806cc0e6228e400/1.png?token-time=2145916800&token-hash=iyiocfousNIUwASmatsIDq8EOsmLUdrQNkWyktHlmJg%3D" alt="Nemo" width="100"></td> <td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/605366/c9dc408fdcbf412fb183ca5b06235f8d/1.jpeg?token-time=2145916800&token-hash=oaqsjLqOFjWN5I9hm2epOaTXaEtKwQUy5OW-EpAz6-g%3D" alt="Jon Leibowitz" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/2.png?token-time=2145916800&token-hash=v0KzlXjyiq18u6aSxyy9qIUxSCAx-nnHlO7MzhSA6Mc%3D" alt="Nesakko" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td> <td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3075183/c2ae575c604e420297f000ccc396e395/1.jpeg?token-time=2145916800&token-hash=O9qmPtpo6wWb0OuvnkEekhk_1WO2MTdytLr7ZgsAr80%3D" alt="Liaizon Wakest" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=20010324">Nemo</a></td> <td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td>
<td><a href="https://www.patreon.com/weepjp">weepjp</a></td> <td><a href="https://www.patreon.com/weepjp">weepjp</a></td>
<td><a href="https://www.patreon.com/jonleibowitz">Jon Leibowitz</a></td>
<td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td> <td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td>
<td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td> <td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
<td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td> <td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td> <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y</a></td>
<td><a href="https://www.patreon.com/AureoleArk">AureoleArk</a></td>
<td><a href="https://www.patreon.com/osapon">osapon</a></td> <td><a href="https://www.patreon.com/osapon">osapon</a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td> <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td> <td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
<td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td> <td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td> <td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpe?token-time=2145916800&token-hash=CPxGQhKIlEaa6WUcgbyHixyKEhakiw9RFdOhsIJBQ_o%3D" alt="takimura" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpe?token-time=2145916800&token-hash=CPxGQhKIlEaa6WUcgbyHixyKEhakiw9RFdOhsIJBQ_o%3D" alt="takimura" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td> <td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha</a></td> <td><a href="https://www.patreon.com/user?u=5827393">motcha</a></td>
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td> <td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
<td><a href="https://www.patreon.com/takimura">takimura</a></td> <td><a href="https://www.patreon.com/takimura">takimura</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td> <td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
<td><a href="https://www.patreon.com/Corset">CG</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td> <td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
<td><a href="https://www.patreon.com/Corset">CG</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td> <td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td> <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td> <td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Sat, 02 Nov 2019 18:09:05 UTC **Last updated:** Sun, 05 Jan 2020 05:37:07 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright
---------------------------------------------------------------- ----------------------------------------------------------------
> Copyright (c) 2014-2019 syuilo > Copyright (c) 2014-2020 syuilo
Misskey is open-source software licensed under the [GNU AGPLv3](LICENSE). Misskey is open-source software licensed under the [GNU AGPLv3](LICENSE).

View File

@ -3,27 +3,22 @@
*/ */
import * as gulp from 'gulp'; import * as gulp from 'gulp';
import * as gutil from 'gulp-util';
import * as ts from 'gulp-typescript'; import * as ts from 'gulp-typescript';
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
import tslint from 'gulp-tslint'; import tslint from 'gulp-tslint';
const cssnano = require('gulp-cssnano');
const stylus = require('gulp-stylus'); const stylus = require('gulp-stylus');
import * as uglifyComposer from 'gulp-uglify/composer';
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import chalk from 'chalk'; import * as chalk from 'chalk';
import * as rename from 'gulp-rename'; import * as rename from 'gulp-rename';
import * as mocha from 'gulp-mocha'; import * as mocha from 'gulp-mocha';
import * as replace from 'gulp-replace'; import * as replace from 'gulp-replace';
const uglifyes = require('uglify-es'); const cleanCSS = require('gulp-clean-css');
const terser = require('gulp-terser');
const locales = require('./locales'); const locales = require('./locales');
const uglify = uglifyComposer(uglifyes, console);
const env = process.env.NODE_ENV || 'development'; const env = process.env.NODE_ENV || 'development';
const isProduction = env === 'production'; const isDebug = env !== 'production';
const isDebug = !isProduction;
if (isDebug) { if (isDebug) {
console.warn(chalk.yellow.bold('WARNING! NODE_ENV is not "production".')); console.warn(chalk.yellow.bold('WARNING! NODE_ENV is not "production".'));
@ -101,17 +96,15 @@ gulp.task('build:client:script', () => {
.pipe(replace('VERSION', JSON.stringify(client.version))) .pipe(replace('VERSION', JSON.stringify(client.version)))
.pipe(replace('ENV', JSON.stringify(env))) .pipe(replace('ENV', JSON.stringify(env)))
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) .pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(isProduction ? uglify({ .pipe(terser({
toplevel: true toplevel: true
} as any) : gutil.noop()) }))
.pipe(gulp.dest('./built/client/assets/')); .pipe(gulp.dest('./built/client/assets/'));
}); });
gulp.task('build:client:styles', () => gulp.task('build:client:styles', () =>
gulp.src('./src/client/app/init.css') gulp.src('./src/client/app/init.css')
.pipe(isProduction .pipe(cleanCSS())
? (cssnano as any)()
: gutil.noop())
.pipe(gulp.dest('./built/client/assets/')) .pipe(gulp.dest('./built/client/assets/'))
); );
@ -130,7 +123,7 @@ gulp.task('copy:client', () =>
gulp.task('doc', () => gulp.task('doc', () =>
gulp.src('./src/docs/**/*.styl') gulp.src('./src/docs/**/*.styl')
.pipe(stylus()) .pipe(stylus())
.pipe((cssnano as any)()) .pipe(cleanCSS())
.pipe(gulp.dest('./built/docs/assets/')) .pipe(gulp.dest('./built/docs/assets/'))
); );

View File

@ -289,6 +289,7 @@ common:
sync: "Synchronizace" sync: "Synchronizace"
save: "Uložit" save: "Uložit"
saved: "Uloženo" saved: "Uloženo"
preview: "Náhled"
room: "Místnost" room: "Místnost"
_room: _room:
graphicsQuality: "Kvalita grafiky" graphicsQuality: "Kvalita grafiky"
@ -399,6 +400,7 @@ common/views/components/games/reversi/reversi.index.vue:
invite: "Pozvat" invite: "Pozvat"
rule: "Jak hrát" rule: "Jak hrát"
mode-invite: "Pozvat" mode-invite: "Pozvat"
invitations: "Jste pozvaní ke hře!"
my-games: "Moje hra" my-games: "Moje hra"
all-games: "Všechny hry" all-games: "Všechny hry"
enter-username: "Zadejte své uživatelské jméno" enter-username: "Zadejte své uživatelské jméno"

View File

@ -258,6 +258,7 @@ common:
load-remote-media: "Vis medie-materiale fra en ekstern server" load-remote-media: "Vis medie-materiale fra en ekstern server"
save: "Gem" save: "Gem"
saved: "Gemt" saved: "Gemt"
preview: "Før-visning"
search: "Søg" search: "Søg"
delete: "Slet" delete: "Slet"
loading: "Henter" loading: "Henter"
@ -1018,6 +1019,8 @@ common/views/components/mute-and-block.vue:
word-mute: "Ordfilter" word-mute: "Ordfilter"
muted-words: "Frafiltrerede ord" muted-words: "Frafiltrerede ord"
muted-words-description: "Mellemrum mellem ord vil blive håndteret, som om der står AND i mellem ordene i søgningen (dvs. alle ord skal være til stede). Linjeskift mellem ord vil føre til, at der søges med OR mellem ordene (dvs. kun det ene af ordene behøver være til stede)." muted-words-description: "Mellemrum mellem ord vil blive håndteret, som om der står AND i mellem ordene i søgningen (dvs. alle ord skal være til stede). Linjeskift mellem ord vil føre til, at der søges med OR mellem ordene (dvs. kun det ene af ordene behøver være til stede)."
unmute-confirm: "Er du sikker på, at du vil fjerne annulleringen af denne bruger?"
unblock-confirm: "Er du sikker på, at du vil fjerne blokeringen af denne bruger?"
save: "Gem" save: "Gem"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
reset: "Skift adgangskode" reset: "Skift adgangskode"
@ -1153,7 +1156,6 @@ admin/views/instance.vue:
object-storage-s3-info-here: "Her" object-storage-s3-info-here: "Her"
object-storage-gcs-info: "Når du bruger Google Cloud Storage som eksternt lager, skal du indstille \"Terminal\" til storage.googleapis.com og forlade feltet \"Region\"." object-storage-gcs-info: "Når du bruger Google Cloud Storage som eksternt lager, skal du indstille \"Terminal\" til storage.googleapis.com og forlade feltet \"Region\"."
cache-remote-files: "Cache eksterne filer" cache-remote-files: "Cache eksterne filer"
cache-remote-files-desc: "Hvis du deaktiverer denne indstilling, kan du linke direkte uden at gemme eksterne filer her. Dermed sparer du plads på din egen server. Til gengæld bliver linksene til de eksterne filer usynlige for brugere, som har deaktiveret direkte links, fordi der ikke vises miniature-billeder. Alt i alt anbefales det at aktivere denne indstilling."
local-drive-capacity-mb: "Kapacitet på hver lokal brugers drev" local-drive-capacity-mb: "Kapacitet på hver lokal brugers drev"
remote-drive-capacity-mb: "Kapacitet på hver ekstern brugers drev" remote-drive-capacity-mb: "Kapacitet på hver ekstern brugers drev"
mb: "I megabytes (MB)" mb: "I megabytes (MB)"
@ -1794,6 +1796,8 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
_round:
arg1: "Tal"
eq: "A og B er ens" eq: "A og B er ens"
_eq: _eq:
arg1: "A" arg1: "A"

View File

@ -251,6 +251,7 @@ common:
load-remote-media: "Zeige Inhalte von fremden Servern" load-remote-media: "Zeige Inhalte von fremden Servern"
save: "Speichern" save: "Speichern"
saved: "Gespeichert" saved: "Gespeichert"
preview: "Vorschau"
search: "Suche" search: "Suche"
delete: "Löschen" delete: "Löschen"
loading: "Laden" loading: "Laden"
@ -743,6 +744,7 @@ common/views/components/drive-settings.vue:
in-use: "benutzt" in-use: "benutzt"
stats: "Statistiken" stats: "Statistiken"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?"
save: "Speichern" save: "Speichern"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
private: "Dieser Beitrag ist privat" private: "Dieser Beitrag ist privat"

View File

@ -300,6 +300,7 @@ common:
sync: "Sync" sync: "Sync"
save: "Save" save: "Save"
saved: "Saved" saved: "Saved"
preview: "Preview"
home-profile: "Home profile" home-profile: "Home profile"
deck-profile: "Deck profile" deck-profile: "Deck profile"
room: "Room" room: "Room"
@ -703,7 +704,7 @@ common/views/components/integration-settings.vue:
title: "Service cooperation" title: "Service cooperation"
connect: "Connect" connect: "Connect"
disconnect: "Disconnect" disconnect: "Disconnect"
connected-to: "You are connected to next account" connected-to: "You are connected to this account"
common/views/components/github-setting.vue: common/views/components/github-setting.vue:
description: "Once you connect your GitHub account to your Misskey account, you will be able to see information about your GitHub account on your profile, and you will be able to sign-in via GitHub." description: "Once you connect your GitHub account to your Misskey account, you will be able to see information about your GitHub account on your profile, and you will be able to sign-in via GitHub."
connected-to: "You are connected to this GitHub account" connected-to: "You are connected to this GitHub account"
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
word-mute: "Word mute" word-mute: "Word mute"
muted-words: "Muted keywords" muted-words: "Muted keywords"
muted-words-description: "Separating with spaces results in AND specifications, and delimiting with line breaks results in OR specifications" muted-words-description: "Separating with spaces results in AND specifications, and delimiting with line breaks results in OR specifications"
unmute-confirm: "Are you certain that you want to unmute this user?"
unblock-confirm: "Are you certain that you want to unblock this user?"
save: "Save" save: "Save"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
reset: "Change password" reset: "Change password"
@ -1276,7 +1279,9 @@ admin/views/instance.vue:
object-storage-s3-info-here: "here" object-storage-s3-info-here: "here"
object-storage-gcs-info: "If you are going to use Google Cloud Storage as Object Storage, Set the 'Endpoint' as storage.googleapis.com, and keep the 'Region' is blank." object-storage-gcs-info: "If you are going to use Google Cloud Storage as Object Storage, Set the 'Endpoint' as storage.googleapis.com, and keep the 'Region' is blank."
cache-remote-files: "Cache remote files" cache-remote-files: "Cache remote files"
cache-remote-files-desc: "Without this parameter, all remote files are linked to their host server directly. This will be an effective solution to save your server storage, however make remote files invisible to users who set direct-link disabled, since no thumbnail will be generated, increase traffic. It is recommended that this parameter set enabled." cache-remote-files-desc: "If disabled, All remote files going to be linked to their origin server directly. This will be an effective solution to save your server storage. However, Since no thumbnail will be generated, It will make increasing data usage, and also may remote files are invisible to users who set direct-link disabled. It is recommended that this config set enabled or enabling the next config, 'Proxy remote files'."
proxy-remote-files: "Proxy remote files"
proxy-remote-files-desc: "If enabled, Remote files that not stored locally or deleted by storage overusage will be proxied locally and also thumbnails will be generated."
local-drive-capacity-mb: "Volume of Drive per user" local-drive-capacity-mb: "Volume of Drive per user"
remote-drive-capacity-mb: "Volume of Drive per remote user" remote-drive-capacity-mb: "Volume of Drive per remote user"
mb: "In megabytes" mb: "In megabytes"
@ -1352,9 +1357,9 @@ admin/views/charts.vue:
charts: charts:
federation-instances: "The number of instances: increase/decrease" federation-instances: "The number of instances: increase/decrease"
federation-instances-total: "Total number of instances" federation-instances-total: "Total number of instances"
notes: "The number of posts: increase/decrease (Combined)" notes: "Increase, or decrease in the number of posts (Combined)"
local-notes: "The number of posts: increase/decrease (Local)" local-notes: "Increase, or decrease in the number of posts (Local)"
remote-notes: "The number of posts: increase/decrease (Remote)" remote-notes: "Increase, or decrease in the number of posts (Remote)"
notes-total: "Total posts" notes-total: "Total posts"
users: "The number of users: increase/decrease" users: "The number of users: increase/decrease"
users-total: "Total users" users-total: "Total users"
@ -1533,7 +1538,7 @@ admin/views/federation.vue:
chart-srcs: chart-srcs:
requests: "Requests" requests: "Requests"
users: "Increase, or decrease in the number of users" users: "Increase, or decrease in the number of users"
users-total: "Total number of users" users-total: "Users in total"
notes: "Increase, or decrease in the number of notes" notes: "Increase, or decrease in the number of notes"
notes-total: "Total number of notes" notes-total: "Total number of notes"
ff: "Increase of followers" ff: "Increase of followers"
@ -1989,6 +1994,9 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
round: "Round decimal"
_round:
arg1: "Number"
eq: "A and B are equal" eq: "A and B are equal"
_eq: _eq:
arg1: "A" arg1: "A"

View File

@ -16,6 +16,8 @@ common:
ui: "Interfaz" ui: "Interfaz"
ui-desc: "No hay ninguna interfaz que le vaya bien a todos. Por eso, Misskey tiene una interfaz altamente personalizable para tus gustos. Puedes hacer tu página principal única editando la interfaz de tu timeline y moviendo varios widgets para conseguir hacer de este lugar uno propio." ui-desc: "No hay ninguna interfaz que le vaya bien a todos. Por eso, Misskey tiene una interfaz altamente personalizable para tus gustos. Puedes hacer tu página principal única editando la interfaz de tu timeline y moviendo varios widgets para conseguir hacer de este lugar uno propio."
drive: "Drive" drive: "Drive"
drive-desc: "¿Quieres postear de nuevo la imagen que has posteado antes? Si es así, ¿Quieres separar y ordenar en carpetas los archivos que has subido? La característica Drive incorporada en la base de Misskey es la solución. Compartir archivos es simple."
outro: "Aún hay características que solamente están en Misskey, asegúrate de eso con tus propios ojos. Misskey es un servicio de red social distribuida, si no te gusta esta instancia, puedes probar otra instancia. Así que, ¡buena suerte!"
application-authorization: "Autorizaciones de la aplicación." application-authorization: "Autorizaciones de la aplicación."
close: "Cerrar" close: "Cerrar"
do-not-copy-paste: "Por favor no copies código aquí. Tu cuenta puede resultar comprometida." do-not-copy-paste: "Por favor no copies código aquí. Tu cuenta puede resultar comprometida."
@ -28,14 +30,26 @@ common:
signin: "Iniciar sesión" signin: "Iniciar sesión"
signup: "¡Regístrate!" signup: "¡Regístrate!"
signout: "Cerrar sesión" signout: "Cerrar sesión"
reload-to-apply-the-setting: "Para aplicar esta configuración, hay que recargar la página. ¿Quiere recargar ahora?"
fetching-as-ap-object: "Consultar en el fediverso"
unfollow-confirm: "¿Quiere dejar de seguir a {name}?"
delete-confirm: "¿Seguro que quieres borrar la publicación?" delete-confirm: "¿Seguro que quieres borrar la publicación?"
signin-required: "Inicie sesion"
notification-type: "Tipo de notificación"
notification-types: notification-types:
all: "Todo" all: "Todo"
pollVote: "Encuestas"
follow: "Seguimientos"
receiveFollowRequest: "Solicitudes de seguimiento"
reply: "Responder" reply: "Responder"
quote: "Citas"
renote: "Volver a publicar" renote: "Volver a publicar"
mention: "Menciones"
reaction: "Reacciones"
got-it: "¡Listo!" got-it: "¡Listo!"
customization-tips: customization-tips:
title: "Consejos de personalización" title: "Consejos de personalización"
paragraph: "<p>Se puede personalizar el inicio agregando/quitando widgets, arrastrarlos, soltarlos y ordenarlos.</p><p>Haciendo <strong>Click <strong>derecho</strong></strong>, se puede modificar la muestra de un widget</p><p>Para quitar un widget, arrastre y suelte el widget en el area que dice <strong>\"Papelera\"</strong> en el cabezal</p><p>Para acabar de personalizar, haga click en \"Listo\" arriba a la derecha</p>"
gotit: "¡Comprendido!" gotit: "¡Comprendido!"
notification: notification:
file-uploaded: "Archivo cargado." file-uploaded: "Archivo cargado."
@ -73,20 +87,42 @@ common:
"write:account": "Editar información de la cuenta" "write:account": "Editar información de la cuenta"
"read:blocks": "Ver bloques" "read:blocks": "Ver bloques"
"write:blocks": "Editar bloques" "write:blocks": "Editar bloques"
"read:drive": "Explorar el drive"
"write:drive": "Administrar el drive"
"read:favorites": "Ver favoritos" "read:favorites": "Ver favoritos"
"write:favorites": "Editar favoritos" "write:favorites": "Editar favoritos"
"read:following": "Ver información de seguidor" "read:following": "Ver información de seguidor"
"write:following": "Seguir/Dejar de seguir"
"read:messaging": "Ver conversación" "read:messaging": "Ver conversación"
"write:messaging": "Administrar coversación"
"read:mutes": "Ver silenciados" "read:mutes": "Ver silenciados"
"write:mutes": "Administrar silenciados"
"write:notes": "Crear y eliminar articulos" "write:notes": "Crear y eliminar articulos"
"read:notifications": "Ver notificaciones" "read:notifications": "Ver notificaciones"
"write:notifications": "Administrar notificaciones"
"read:reactions": "Ver reacciones" "read:reactions": "Ver reacciones"
"write:reactions": "Administrar reacciones"
"write:votes": "Vota" "write:votes": "Vota"
"read:pages": "Ver páginas"
"write:pages": "Administrar páginas"
"read:page-likes": "Ver páginas que te gustan"
"write:page-likes": "Administrar páginas que te gustan"
"read:user-groups": "Ver grupos de usuarios"
"write:user-groups": "Administrar grupos de usuarios"
empty-timeline-info:
follow-users-to-make-your-timeline: "Seguir al usuario mostrará sus posts en la linea de tiempo"
explore: "Explorar usuarios"
post-form: post-form:
reply: "Responder" reply: "Responder"
renote: "Volver a publicar" renote: "Volver a publicar"
attach-media-from-local: "Agregar medios de tu dispositivo"
insert-a-kao: "v('ω')v"
recent-tags: "Reciente"
error: "Error"
enter-username: "Ingresar nombre de usuario" enter-username: "Ingresar nombre de usuario"
add-visible-user: "Agregar usuario"
username-prompt: "Ingresar nombre de usuario" username-prompt: "Ingresar nombre de usuario"
enter-file-name: "Editar nombre del archivo"
weekday-short: weekday-short:
sunday: "domingo" sunday: "domingo"
monday: "lunes" monday: "lunes"
@ -141,15 +177,18 @@ common:
mute-and-block: "Silenciar/Bloquear" mute-and-block: "Silenciar/Bloquear"
blocking: "Bloquear" blocking: "Bloquear"
security: "Seguridad" security: "Seguridad"
signin: "Historial de ingresos"
password: "Contraseña" password: "Contraseña"
other: "Otros" other: "Otros"
appearance: "Diseño" appearance: "Diseño"
behavior: "Comportamiento" behavior: "Comportamiento"
reactions: "Reacciones"
fetch-on-scroll-desc: "Cuando te deslizas al final de la página nuevo contenido se carga automáticamente." fetch-on-scroll-desc: "Cuando te deslizas al final de la página nuevo contenido se carga automáticamente."
note-visibility: "Visibilidad de la publicación" note-visibility: "Visibilidad de la publicación"
default-note-visibility: "Rango de publicación predeterminado" default-note-visibility: "Rango de publicación predeterminado"
web-search-engine: "Buscador web" web-search-engine: "Buscador web"
web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}" web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}"
keep-cw: "Mantener CW"
this-setting-is-this-device-only: "Solo para este dispositivo" this-setting-is-this-device-only: "Solo para este dispositivo"
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo" use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
line-width: "Grosor de línea" line-width: "Grosor de línea"
@ -201,6 +240,7 @@ common:
navbar-position-left: "Izquierda" navbar-position-left: "Izquierda"
save: "Guardar" save: "Guardar"
saved: "Guardado" saved: "Guardado"
preview: "Vista previa"
search: "Buscar" search: "Buscar"
delete: "eliminar" delete: "eliminar"
loading: "cargando" loading: "cargando"
@ -868,6 +908,7 @@ desktop/views/components/settings.tags.vue:
desktop/views/components/timeline.vue: desktop/views/components/timeline.vue:
home: "Inicio" home: "Inicio"
local: "Local" local: "Local"
hybrid: "Social"
global: "Global" global: "Global"
list: "Listas" list: "Listas"
hashtag: "Hashtags" hashtag: "Hashtags"
@ -1006,6 +1047,8 @@ admin/views/federation.vue:
day: "Por día" day: "Por día"
blocked-hosts: "Bloquear" blocked-hosts: "Bloquear"
save: "Guardar" save: "Guardar"
desktop/views/pages/welcome.vue:
timeline: "Timeline"
desktop/views/pages/selectdrive.vue: desktop/views/pages/selectdrive.vue:
cancel: "Cancelar" cancel: "Cancelar"
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
@ -1020,6 +1063,11 @@ desktop/views/pages/user/user.photos.vue:
desktop/views/pages/user/user.header.vue: desktop/views/pages/user/user.header.vue:
month: "lunes" month: "lunes"
day: "domingo" day: "domingo"
desktop/views/pages/user/user.timeline.vue:
default: "Posts"
with-replies: "Posts y respuestas"
with-media: "Multimedia"
my-posts: "Mis posts"
desktop/views/widgets/notifications.vue: desktop/views/widgets/notifications.vue:
title: "Notificaciones" title: "Notificaciones"
desktop/views/widgets/polls.vue: desktop/views/widgets/polls.vue:
@ -1066,6 +1114,7 @@ mobile/views/components/ui.header.vue:
welcome-back: "Bienvenido/a de vuelta," welcome-back: "Bienvenido/a de vuelta,"
adjective: "-san" adjective: "-san"
mobile/views/components/ui.nav.vue: mobile/views/components/ui.nav.vue:
timeline: "Timeline"
notifications: "Notificaciones" notifications: "Notificaciones"
follow-requests: "Solicitudes de seguimiento" follow-requests: "Solicitudes de seguimiento"
search: "Buscar" search: "Buscar"
@ -1080,6 +1129,7 @@ mobile/views/pages/drive.vue:
mobile/views/pages/home.vue: mobile/views/pages/home.vue:
home: "Inicio" home: "Inicio"
local: "Local" local: "Local"
hybrid: "Social"
global: "Global" global: "Global"
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "Panel de control" dashboard: "Panel de control"
@ -1093,6 +1143,8 @@ mobile/views/pages/search.vue:
search: "Buscar" search: "Buscar"
mobile/views/pages/notifications.vue: mobile/views/pages/notifications.vue:
notifications: "Notificaciones" notifications: "Notificaciones"
mobile/views/pages/user.vue:
timeline: "Timeline"
mobile/views/pages/user/home.vue: mobile/views/pages/user/home.vue:
activity: "Actividad" activity: "Actividad"
mobile/views/pages/user/home.photos.vue: mobile/views/pages/user/home.photos.vue:
@ -1100,6 +1152,7 @@ mobile/views/pages/user/home.photos.vue:
deck: deck:
home: "Inicio" home: "Inicio"
local: "Local" local: "Local"
hybrid: "Social"
hashtag: "Etiquetas" hashtag: "Etiquetas"
global: "Global" global: "Global"
notifications: "Notificaciones" notifications: "Notificaciones"
@ -1107,6 +1160,7 @@ deck:
rename: "Renombrar" rename: "Renombrar"
deck/deck.user-column.vue: deck/deck.user-column.vue:
activity: "Actividad" activity: "Actividad"
timeline: "Timeline"
pages: pages:
pin-this-page: "Fijar en el perfil" pin-this-page: "Fijar en el perfil"
like: "Me gusta" like: "Me gusta"

View File

@ -31,6 +31,7 @@ common:
signup: "S'enregistrer" signup: "S'enregistrer"
signout: "Se déconnecter" signout: "Se déconnecter"
reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?" reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?"
fetching-as-ap-object: "Récupération depuis le fédiverse"
unfollow-confirm: "Désirez-vous vous désabonner de {name} ?" unfollow-confirm: "Désirez-vous vous désabonner de {name} ?"
delete-confirm: "Supprimer cette publication ?" delete-confirm: "Supprimer cette publication ?"
signin-required: "Veuillez vous connecter" signin-required: "Veuillez vous connecter"
@ -48,6 +49,7 @@ common:
got-it: "Jai compris !" got-it: "Jai compris !"
customization-tips: customization-tips:
title: "Conseils de personnalisation" title: "Conseils de personnalisation"
paragraph: "<p>La personnalisation de la page d'accueil vous permet d'ajouter/supprimer, glisser-déposer et réarranger les widgets.</p><p>Vous pouvez changer l'apparence de certain widget avec le <strong><strong>clic</strong>droit</strong>.</p><p>Pour supprimer un widget, faites glisser le widget sur <strong>la zone \"Corbeille\"</strong> dans l'en-tête.</p><p>Pour terminer la personnalisation, cliquez sur \"Terminé\" en haut à droite.</p>"
gotit: "Compris !" gotit: "Compris !"
notification: notification:
file-uploaded: "Le fichier a été téléversé !" file-uploaded: "Le fichier a été téléversé !"
@ -90,9 +92,11 @@ common:
"read:favorites": "Afficher les favoris" "read:favorites": "Afficher les favoris"
"write:favorites": "Écrire des favoris" "write:favorites": "Écrire des favoris"
"read:following": "Voir les informations de l'abonné" "read:following": "Voir les informations de l'abonné"
"write:following": "Suivre/Ne plus suivre"
"read:messaging": "Lire les conversations" "read:messaging": "Lire les conversations"
"write:messaging": "Utiliser la messagerie" "write:messaging": "Utiliser la messagerie"
"read:mutes": "Voir les comptes masqués" "read:mutes": "Voir les comptes masqués"
"write:mutes": "Gérer les comptes muets"
"write:notes": "Créer ou supprimer des publications" "write:notes": "Créer ou supprimer des publications"
"read:notifications": "Afficher les notifications" "read:notifications": "Afficher les notifications"
"write:notifications": "Gérer vos notifications" "write:notifications": "Gérer vos notifications"
@ -100,6 +104,11 @@ common:
"write:reactions": "Gérer vos réactions" "write:reactions": "Gérer vos réactions"
"write:votes": "Vote" "write:votes": "Vote"
"read:pages": "Afficher la page" "read:pages": "Afficher la page"
"write:pages": "Mettre à jour les Pages"
"read:page-likes": "Lire les favoris sur les Pages"
"write:page-likes": "Mettre à jour les favoris sur les Pages"
"read:user-groups": "Voir les groupes d'utilisateur·rice·s"
"write:user-groups": "Éditer les groupes des utilisateur·rice·s"
empty-timeline-info: empty-timeline-info:
follow-users-to-make-your-timeline: "Les utilisateur·rice·s suivant·e·s afficheront leurs publications sur votre fil." follow-users-to-make-your-timeline: "Les utilisateur·rice·s suivant·e·s afficheront leurs publications sur votre fil."
explore: "Trouver des utilisateur·rice·s" explore: "Trouver des utilisateur·rice·s"
@ -110,6 +119,7 @@ common:
quote-placeholder: "Citer cette note …" quote-placeholder: "Citer cette note …"
option-quote-placeholder: "Citer ce billet ... (Facultatif)" option-quote-placeholder: "Citer ce billet ... (Facultatif)"
quote-attached: "Cité" quote-attached: "Cité"
quote-question: "Souhaitez-vous ajoutez une citation ?"
submit: "Publication" submit: "Publication"
reply: "Répondre" reply: "Répondre"
renote: "Republier" renote: "Republier"
@ -126,6 +136,7 @@ common:
geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation" geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation"
error: "Erreur" error: "Erreur"
enter-username: "Saisir un nom d'utilisateur" enter-username: "Saisir un nom d'utilisateur"
specified-recipient: "Correspondant·e"
add-visible-user: "Ajouter un utilisateur" add-visible-user: "Ajouter un utilisateur"
cw-placeholder: "Commenter le contenu (optionnel)" cw-placeholder: "Commenter le contenu (optionnel)"
username-prompt: "Saisir un nom d'utilisateur" username-prompt: "Saisir un nom d'utilisateur"
@ -190,6 +201,7 @@ common:
appearance: "Apparence" appearance: "Apparence"
behavior: "Comportement" behavior: "Comportement"
reactions: "Réaction" reactions: "Réaction"
reactions-description: "Personnaliser les émojis à afficher dans le sélecteur de réactions, délimités par les sauts de ligne."
fetch-on-scroll: "Chargement automatique lors du défilement" fetch-on-scroll: "Chargement automatique lors du défilement"
fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page." fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
note-visibility: "Visibilité de la publication" note-visibility: "Visibilité de la publication"
@ -198,7 +210,9 @@ common:
web-search-engine: "Moteur de recherche Web" web-search-engine: "Moteur de recherche Web"
web-search-engine-desc: "Exemple: https://www.google.com/?#q={{query}}" web-search-engine-desc: "Exemple: https://www.google.com/?#q={{query}}"
paste: "Coller" paste: "Coller"
pasted-file-name: "Modèle de nom de fichier collé"
pasted-file-name-desc: "Exemple : \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\"" pasted-file-name-desc: "Exemple : \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
paste-dialog: "Modifier le nom du fichier collé"
keep-cw: "Maintenir l'avertissement de contenu" keep-cw: "Maintenir l'avertissement de contenu"
keep-cw-desc: "Lorsque vous répondez à un message, le même avertissement de contenu est reprit par défaut dans la réponse, le même que celui qui a été défini dans le message original." keep-cw-desc: "Lorsque vous répondez à un message, le même avertissement de contenu est reprit par défaut dans la réponse, le même que celui qui a été défini dans le message original."
i-like-sushi: "Je préfère les sushis plutôt que le pudding" i-like-sushi: "Je préfère les sushis plutôt que le pudding"
@ -206,6 +220,7 @@ common:
use-avatar-reversi-stones: "Utiliser lavatar comme pion dans Reversi" use-avatar-reversi-stones: "Utiliser lavatar comme pion dans Reversi"
disable-animated-mfm: "Désactiver les textes animés dans les publications" disable-animated-mfm: "Désactiver les textes animés dans les publications"
disable-showing-animated-images: "Désactiver l'animation des images" disable-showing-animated-images: "Désactiver l'animation des images"
enable-quick-notification-view: "Activer l'affichage rapide des notifications"
suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie" suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie"
always-show-nsfw: "Toujours afficher les contenus sensibles" always-show-nsfw: "Toujours afficher les contenus sensibles"
always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles" always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles"
@ -284,6 +299,7 @@ common:
sync: "Synchroniser" sync: "Synchroniser"
save: "Enregistrer" save: "Enregistrer"
saved: "enregistré" saved: "enregistré"
preview: "Prévisualisation"
home-profile: "Profil principal" home-profile: "Profil principal"
deck-profile: "Profil deck" deck-profile: "Profil deck"
room: "Pièce" room: "Pièce"
@ -376,9 +392,11 @@ common/views/pages/explore.vue:
popular-users: "Utilisateur·rice·s populaires" popular-users: "Utilisateur·rice·s populaires"
recently-updated-users: "Utilisateur·rice·s actif·ve·s récemment" recently-updated-users: "Utilisateur·rice·s actif·ve·s récemment"
recently-registered-users: "Les nouveaux inscrits" recently-registered-users: "Les nouveaux inscrits"
recently-discovered-users: "Utilisateurs récemment découverts"
popular-tags: "Mots-clés populaires" popular-tags: "Mots-clés populaires"
federated: "Du Fédiverse" federated: "Du Fédiverse"
explore: "Explorer {host}" explore: "Explorer {host}"
explore-fediverse: "Explorer le Fédiverse"
users-info: "Actuellement, {users} utilisateur·rice·s se sont inscrit ici" users-info: "Actuellement, {users} utilisateur·rice·s se sont inscrit ici"
common/views/components/reactions-viewer.details.vue: common/views/components/reactions-viewer.details.vue:
few-users: "{users} ont réagit avec {reaction}" few-users: "{users} ont réagit avec {reaction}"
@ -548,6 +566,7 @@ common/views/components/note-menu.vue:
delete: "Supprimer" delete: "Supprimer"
delete-confirm: "Supprimer cette publication ?" delete-confirm: "Supprimer cette publication ?"
delete-and-edit: "Supprimer et réécrire" delete-and-edit: "Supprimer et réécrire"
delete-and-edit-confirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses."
remote: "Afficher la note originale" remote: "Afficher la note originale"
pin-limit-exceeded: "Vous ne pouvez plus épingler davantage de publications." pin-limit-exceeded: "Vous ne pouvez plus épingler davantage de publications."
common/views/components/user-menu.vue: common/views/components/user-menu.vue:
@ -571,6 +590,7 @@ common/views/components/user-menu.vue:
suspend: "Suspendre" suspend: "Suspendre"
unsuspend: "Ne plus suspendre" unsuspend: "Ne plus suspendre"
suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?" suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?"
unsuspend-confirm: "Êtes-vous sûr de vouloir débloquer cet utilisateur ?"
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "Voter pour '{}'" vote-to: "Voter pour '{}'"
vote-count: "{} votes" vote-count: "{} votes"
@ -593,6 +613,7 @@ common/views/components/poll-editor.vue:
expiration: "Valide jusqu'à" expiration: "Valide jusqu'à"
infinite: "Illimité" infinite: "Illimité"
at: "Choisir une date et une durée" at: "Choisir une date et une durée"
after: "Choisir la durée"
no-more: "Vous ne pouvez pas en ajouter davantage" no-more: "Vous ne pouvez pas en ajouter davantage"
deadline-date: "Date déchéance" deadline-date: "Date déchéance"
deadline-time: "Durée" deadline-time: "Durée"
@ -606,7 +627,9 @@ common/views/components/reaction-picker.vue:
choose-reaction: "Envoyer une réaction" choose-reaction: "Envoyer une réaction"
input-reaction-placeholder: "ou insérez un émoji" input-reaction-placeholder: "ou insérez un émoji"
common/views/components/emoji-picker.vue: common/views/components/emoji-picker.vue:
recent-emoji: "Utilisés récemment"
custom-emoji: "Émoji personnalisé" custom-emoji: "Émoji personnalisé"
no-category: "Sans catégorie"
people: "Personnes" people: "Personnes"
animals-and-nature: "Animaux et nature" animals-and-nature: "Animaux et nature"
food-and-drink: "Nourriture et boisson" food-and-drink: "Nourriture et boisson"
@ -802,6 +825,7 @@ common/views/widgets/broadcast.vue:
no-broadcasts: "Aucune annonce" no-broadcasts: "Aucune annonce"
have-a-nice-day: "Passez une bonne journée !" have-a-nice-day: "Passez une bonne journée !"
next: "Suivant" next: "Suivant"
prev: "Précédent"
common/views/widgets/calendar.vue: common/views/widgets/calendar.vue:
year: "Année {}" year: "Année {}"
month: "{}," month: "{},"
@ -846,6 +870,7 @@ common/views/widgets/tips.vue:
tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur." tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur."
tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé" tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé"
tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots." tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots."
tips-line23: "Ai-chan kawaii!"
tips-line24: "Misskey est fonctionnel depuis 2014" tips-line24: "Misskey est fonctionnel depuis 2014"
tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible" tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible"
common/views/pages/not-found.vue: common/views/pages/not-found.vue:
@ -1044,6 +1069,7 @@ desktop/views/components/settings.2fa.vue:
info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil." info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil."
totp-header: "Application d'authentification" totp-header: "Application d'authentification"
security-key-header: "Clé de sécurité" security-key-header: "Clé de sécurité"
security-key: "Pour plus de sécurité, vous pouvez vous connecter à votre compte à l'aide d'une clé de sécurité matérielle qui prend en charge FIDO2. Lorsque vous vous connecterez, vous aurez besoin de la clé de sécurité enregistrée ou d'une application d'authentification avec vous."
last-used: "Dernière utilisation :" last-used: "Dernière utilisation :"
activate-key: "Cliquez pour activer la clé de sécurité" activate-key: "Cliquez pour activer la clé de sécurité"
security-key-name: "Nom de la clé" security-key-name: "Nom de la clé"
@ -1086,6 +1112,8 @@ common/views/components/mute-and-block.vue:
word-mute: "Filtre de mots" word-mute: "Filtre de mots"
muted-words: "Mots masqués" muted-words: "Mots masqués"
muted-words-description: "Description des mots mis en sourdine" muted-words-description: "Description des mots mis en sourdine"
unmute-confirm: "Ne plus masquer cet utilisateur ?"
unblock-confirm: "Débloquer cet utilisateur ?"
save: "Enregistrer" save: "Enregistrer"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
reset: "Modifier le mot de passe" reset: "Modifier le mot de passe"
@ -1167,6 +1195,8 @@ admin/views/index.vue:
back-to-misskey: "Retour vers Misskey" back-to-misskey: "Retour vers Misskey"
admin/views/db.vue: admin/views/db.vue:
tables: "Tables" tables: "Tables"
vacuum: "Vacuum"
vacuum-info: "Range la base de données. Conserve les données intactes et réduit l'utilisation du disque. Cela se fait généralement automatiquement et périodiquement."
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "Tableau de bord" dashboard: "Tableau de bord"
accounts: "Comptes" accounts: "Comptes"
@ -1269,6 +1299,7 @@ admin/views/instance.vue:
discord-integration-client-id: "ID client" discord-integration-client-id: "ID client"
discord-integration-client-secret: "Secret client" discord-integration-client-secret: "Secret client"
proxy-account-config: "Compte proxy" proxy-account-config: "Compte proxy"
proxy-account-info: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant pour les utilisateurs d'autres instances.\nExemple : quand un·e utilisateur·rice distant·e est ajouté·e à une liste, ses publications ne serait pas visibles sur l'instance si personne ne le·la suit. Le compte proxy va donc le·la suivre pour que ses publications soient acheminées."
proxy-account-username: "Nom dutilisateur du compte proxy" proxy-account-username: "Nom dutilisateur du compte proxy"
proxy-account-username-desc: "Spécifiez le nom dutilisateur du compte utilisé comme proxy." proxy-account-username-desc: "Spécifiez le nom dutilisateur du compte utilisé comme proxy."
proxy-account-warn: "Avant dentamer cette action, vous devez au préalable avoir créé un compte avec ce nom dutilisateur." proxy-account-warn: "Avant dentamer cette action, vous devez au préalable avoir créé un compte avec ce nom dutilisateur."
@ -1350,6 +1381,7 @@ admin/views/drive.vue:
marked-as-sensitive: "Marqué comme sensible" marked-as-sensitive: "Marqué comme sensible"
unmarked-as-sensitive: "Marqué comme non sensible" unmarked-as-sensitive: "Marqué comme non sensible"
clean-remote-files: "Nettoyer le cache des fichiers distants" clean-remote-files: "Nettoyer le cache des fichiers distants"
clean-remote-files-are-you-sure: "Êtes-vous sûr de vouloir effacer tout les fichiers distants mis en cache ?"
clean-up: "Nettoyage" clean-up: "Nettoyage"
admin/views/users.vue: admin/views/users.vue:
operation: "Actions" operation: "Actions"
@ -1416,6 +1448,7 @@ admin/views/emoji.vue:
title: "Ajouter un émoji" title: "Ajouter un émoji"
name: "Nom de lémoji" name: "Nom de lémoji"
name-desc: "Vous pouvez utiliser les caractères a~z 0~9 _" name-desc: "Vous pouvez utiliser les caractères a~z 0~9 _"
category: "Catégories"
aliases: "Aliases" aliases: "Aliases"
aliases-desc: "Vous pouvez définir plus dun, séparés par des espaces." aliases-desc: "Vous pouvez définir plus dun, séparés par des espaces."
url: "URL de limage" url: "URL de limage"
@ -1488,6 +1521,7 @@ admin/views/federation.vue:
notes: "Augmentation/diminution du nombre des notes" notes: "Augmentation/diminution du nombre des notes"
notes-total: "Nombre total des notes" notes-total: "Nombre total des notes"
ff: "Augmentation des abonné·e·s" ff: "Augmentation des abonné·e·s"
ff-total: "Nombre total d'abonnements"
drive-usage: "Augmentation et diminution de la capacité stockage" drive-usage: "Augmentation et diminution de la capacité stockage"
drive-usage-total: "Utilisation totale du stockage" drive-usage-total: "Utilisation totale du stockage"
drive-files-total: "Nombre total des fichiers sur le Drive" drive-files-total: "Nombre total des fichiers sur le Drive"
@ -1881,9 +1915,11 @@ pages:
value: "Valeur" value: "Valeur"
fn: "Fonction" fn: "Fonction"
text: "Actions texte" text: "Actions texte"
convert: "Convertir"
list: "Listes" list: "Listes"
blocks: blocks:
text: "Texte" text: "Texte"
multiLineText: "Texte (Multi-lignes)"
textList: "Liste de texte" textList: "Liste de texte"
strLen: "Longueur du texte" strLen: "Longueur du texte"
_strLen: _strLen:
@ -1891,6 +1927,8 @@ pages:
strPick: "Extraire un caractère" strPick: "Extraire un caractère"
_strPick: _strPick:
arg1: "Texte" arg1: "Texte"
arg2: "Position du joueur"
strReplace: "Remplacement de texte"
_strReplace: _strReplace:
arg1: "Texte" arg1: "Texte"
arg2: "Avant le remplacement" arg2: "Avant le remplacement"
@ -1920,6 +1958,8 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
_round:
arg1: "Numérique"
eq: "A et B sont équivalents" eq: "A et B sont équivalents"
_eq: _eq:
arg1: "A" arg1: "A"
@ -1952,6 +1992,7 @@ pages:
_gtEq: _gtEq:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
if: "Branche"
_if: _if:
arg1: "Si" arg1: "Si"
arg2: "donc" arg2: "donc"
@ -1969,6 +2010,7 @@ pages:
randomPick: "Choisir aléatoirement depuis la liste" randomPick: "Choisir aléatoirement depuis la liste"
_randomPick: _randomPick:
arg1: "Listes" arg1: "Listes"
dailyRandom: "Aléatoire (Quotidien pour chaque utilisateur)"
_dailyRandom: _dailyRandom:
arg1: "Probabilité" arg1: "Probabilité"
_dailyRannum: _dailyRannum:
@ -1976,19 +2018,27 @@ pages:
arg2: "Maximum" arg2: "Maximum"
_dailyRandomPick: _dailyRandomPick:
arg1: "Listes" arg1: "Listes"
seedRandom: "Aléatoire (graine)"
_seedRandom: _seedRandom:
arg1: "Graine"
arg2: "Probabilité" arg2: "Probabilité"
seedRannum: "Nombre aléatoire (Graine)"
_seedRannum: _seedRannum:
arg1: "Graine"
arg2: "Min" arg2: "Min"
arg3: "Max" arg3: "Max"
seedRandomPick: "Sélection aléatoire dans une liste (Graine)"
_seedRandomPick: _seedRandomPick:
arg1: "Graine"
arg2: "Listes" arg2: "Listes"
DRPWPM: "Sélection aléatoire à partir d'une liste pondérée (mise à jour quotidienne par utilisateur)"
_DRPWPM: _DRPWPM:
arg1: "Liste de texte" arg1: "Liste de texte"
pick: "Sélectionner dans la liste" pick: "Sélectionner dans la liste"
_pick: _pick:
arg1: "Listes" arg1: "Listes"
arg2: "Position" arg2: "Position"
listLen: "Longueur de la liste"
_listLen: _listLen:
arg1: "Listes" arg1: "Listes"
number: "Numérique" number: "Numérique"
@ -1998,12 +2048,14 @@ pages:
numberToString: "Chiffres en chaîne" numberToString: "Chiffres en chaîne"
_numberToString: _numberToString:
arg1: "Numérique" arg1: "Numérique"
splitStrByLine: "Séparer le texte par lignes"
_splitStrByLine: _splitStrByLine:
arg1: "Texte" arg1: "Texte"
ref: "Variables" ref: "Variables"
fn: "Fonction" fn: "Fonction"
_fn: _fn:
slots: "Emplacement" slots: "Emplacement"
slots-info: "Veuillez délimiter chaque emplacement par un saut de ligne"
arg1: "Sortie" arg1: "Sortie"
for: "Répéter" for: "Répéter"
_for: _for:
@ -2019,10 +2071,12 @@ pages:
emptySlot: "Slot vide" emptySlot: "Slot vide"
enviromentVariables: "Variables d'environnement" enviromentVariables: "Variables d'environnement"
pageVariables: "Élément de page" pageVariables: "Élément de page"
argVariables: "Entrée vide"
room: room:
add-furniture: "Placer des meubles" add-furniture: "Placer des meubles"
translate: "Déplacer" translate: "Déplacer"
rotate: "Tourner" rotate: "Tourner"
exit: "Retour"
remove: "Enlever" remove: "Enlever"
save: "Enregistrer" save: "Enregistrer"
saved: "enregistré" saved: "enregistré"
@ -2048,6 +2102,7 @@ room:
plant2: "Plante dintérieur 2" plant2: "Plante dintérieur 2"
eraser: "Gomme" eraser: "Gomme"
pencil: "Crayon" pencil: "Crayon"
pudding: "Pudding"
cardboard-box: "Boîte en carton" cardboard-box: "Boîte en carton"
cardboard-box2: "Boîte en carton 2" cardboard-box2: "Boîte en carton 2"
cardboard-box3: "Boîte en carton 3" cardboard-box3: "Boîte en carton 3"
@ -2062,6 +2117,7 @@ room:
monitor: "Écran" monitor: "Écran"
keyboard: "Clavier" keyboard: "Clavier"
carpet-stripe: "Tapis (zébré)" carpet-stripe: "Tapis (zébré)"
mat: "Tapis"
color-box: "Étagère" color-box: "Étagère"
wall-clock: "Horloge murale" wall-clock: "Horloge murale"
photoframe: "Cadre photo" photoframe: "Cadre photo"

View File

@ -1225,6 +1225,8 @@ common/views/components/mute-and-block.vue:
word-mute: "ワードミュート" word-mute: "ワードミュート"
muted-words: "ミュートされたキーワード" muted-words: "ミュートされたキーワード"
muted-words-description: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" muted-words-description: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
unmute-confirm: "このユーザーをミュート解除しますか?"
unblock-confirm: "このユーザーをブロック解除しますか?"
save: "保存" save: "保存"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
@ -1408,7 +1410,9 @@ admin/views/instance.vue:
object-storage-s3-info-here: "こちら" object-storage-s3-info-here: "こちら"
object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。" object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。"
cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにするか次のリモートファイルのプロキシを有効にすることをおすすめします。"
proxy-remote-files: "リモートのファイルをプロキシする"
proxy-remote-files-desc: "この設定を有効にすると、未保存または保存容量超過で削除されたリモートファイルをローカルでプロキシし、サムネイルも生成するようになります。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量" remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
mb: "メガバイト単位" mb: "メガバイト単位"
@ -2198,6 +2202,9 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
round: "小数を丸める"
_round:
arg1: "数値"
eq: "AとBが同じ" eq: "AとBが同じ"
_eq: _eq:
arg1: "A" arg1: "A"

View File

@ -130,6 +130,7 @@ common:
timeline: "タイムライン" timeline: "タイムライン"
save: "保存" save: "保存"
saved: "保存したで!" saved: "保存したで!"
preview: "試してみる"
search: "検索" search: "検索"
delete: "削除" delete: "削除"
loading: "読み込み中" loading: "読み込み中"
@ -866,7 +867,6 @@ admin/views/instance.vue:
drive-config: "ドライブの設定" drive-config: "ドライブの設定"
object-storage-endpoint: "エンドポイント" object-storage-endpoint: "エンドポイント"
cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files: "リモートのファイルをキャッシュする"
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。"
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量" remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
mb: "メガバイト単位" mb: "メガバイト単位"

View File

@ -300,6 +300,7 @@ common:
sync: "동기화" sync: "동기화"
save: "저장" save: "저장"
saved: "저장하였습니다" saved: "저장하였습니다"
preview: "미리보기"
home-profile: "홈 프로필" home-profile: "홈 프로필"
deck-profile: "덱 프로필" deck-profile: "덱 프로필"
room: "룸" room: "룸"
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
word-mute: "단어 뮤트" word-mute: "단어 뮤트"
muted-words: "뮤트된 키워드" muted-words: "뮤트된 키워드"
muted-words-description: "공백으로 구분하는 경우 AND로 지정되며, 줄바꿈으로 구분하는 경우 OR로 지정됩니다" muted-words-description: "공백으로 구분하는 경우 AND로 지정되며, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
unmute-confirm: "이 사용자를 뮤트 해제하시겠습니까?"
unblock-confirm: "이 사용자를 차단 해제하시겠습니까?"
save: "저장" save: "저장"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
reset: "비밀번호 변경" reset: "비밀번호 변경"
@ -1276,7 +1279,9 @@ admin/views/instance.vue:
object-storage-s3-info-here: "이곳" object-storage-s3-info-here: "이곳"
object-storage-gcs-info: "Google Cloud Storage를 오브젝트 스토리지로 사용하는 경우, 「엔드포인트」는 storage.googleapis.com 으로 설정하고, 「리전」 란은 비웁니다." object-storage-gcs-info: "Google Cloud Storage를 오브젝트 스토리지로 사용하는 경우, 「엔드포인트」는 storage.googleapis.com 으로 설정하고, 「리전」 란은 비웁니다."
cache-remote-files: "원격 파일을 캐시" cache-remote-files: "원격 파일을 캐시"
cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다." cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 보통은 이 설정을 사용하거나 아래의 원격 파일 프록시를 설정하는 것을 추천합니다."
proxy-remote-files: "원격 파일 프록시"
proxy-remote-files-desc: "이 설정을 사용하면, 저장되지 않았거나 용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다."
local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량" local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량"
remote-drive-capacity-mb: "원격 사용자 한 명당 드라이브 용량" remote-drive-capacity-mb: "원격 사용자 한 명당 드라이브 용량"
mb: "메가바이트 단위" mb: "메가바이트 단위"
@ -1989,6 +1994,9 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
round: "소수점을 반올림"
_round:
arg1: "수치"
eq: "A와 B가 동일" eq: "A와 B가 동일"
_eq: _eq:
arg1: "A" arg1: "A"

View File

@ -162,6 +162,7 @@ common:
note-visibility: "Widoczność wpisów" note-visibility: "Widoczność wpisów"
remember-note-visibility: "Zapamiętaj widoczność wpisów" remember-note-visibility: "Zapamiętaj widoczność wpisów"
web-search-engine: "Wyszukiwarka internetowa" web-search-engine: "Wyszukiwarka internetowa"
web-search-engine-desc: "Wzór: https://www.google.com/?#q={{query}}"
paste: "Wklej" paste: "Wklej"
line-width: "Szerokości linii" line-width: "Szerokości linii"
line-width-thin: "Cienka" line-width-thin: "Cienka"
@ -193,6 +194,7 @@ common:
navbar-position-left: "Z lewej" navbar-position-left: "Z lewej"
save: "Zapisz" save: "Zapisz"
saved: "Zapisano" saved: "Zapisano"
preview: "Pokaż podgląd"
search: "Szukaj" search: "Szukaj"
delete: "Usuń" delete: "Usuń"
loading: "Ładowanie" loading: "Ładowanie"

View File

@ -12,7 +12,7 @@ common:
rich-contents: "Посты" rich-contents: "Посты"
rich-contents-desc: "Просто выложи свою идею, актуальные темы и всё, что тебе хочется показать миру. Ты можешь декорировать свои слова, прикреплять свои любимые картинки, отправлять файлы с фильмами и создать голосование - это те вещи, которые ты можешь сделать с помощью Misskey!" rich-contents-desc: "Просто выложи свою идею, актуальные темы и всё, что тебе хочется показать миру. Ты можешь декорировать свои слова, прикреплять свои любимые картинки, отправлять файлы с фильмами и создать голосование - это те вещи, которые ты можешь сделать с помощью Misskey!"
reaction: "Реакции" reaction: "Реакции"
reaction-desc: "Самый лёгкий способ выразить свои эмоции. Misskey позволяет добавлять различные виды реакций к постам других людей. Эмоциональный опыт из Misskey никогда не появится в других социальных сетях, позволяющих только жать “лайки”." reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
ui: "Интерфейс" ui: "Интерфейс"
ui-desc: "Нет такого интерфейса, понравившегося всем. Поэтому у Misskey имеется пользовательский интерфейс, широко настраиваемый под ваши вкусы. Создай себе уникальную домашнюю страницу редактируя, подстраивая оформление ленты и размещая виджеты, которые тоже можно кастомизировать." ui-desc: "Нет такого интерфейса, понравившегося всем. Поэтому у Misskey имеется пользовательский интерфейс, широко настраиваемый под ваши вкусы. Создай себе уникальную домашнюю страницу редактируя, подстраивая оформление ленты и размещая виджеты, которые тоже можно кастомизировать."
drive: "Хранилище файлов" drive: "Хранилище файлов"

View File

@ -300,6 +300,7 @@ common:
sync: "同步" sync: "同步"
save: "保存" save: "保存"
saved: "已保存" saved: "已保存"
preview: "预览"
home-profile: "定制首页数据" home-profile: "定制首页数据"
deck-profile: "定制Deck数据" deck-profile: "定制Deck数据"
room: "房间" room: "房间"
@ -363,7 +364,7 @@ common:
notifications: "通知" notifications: "通知"
users: "推荐用户" users: "推荐用户"
polls: "调查问卷" polls: "调查问卷"
post-form: "投稿形式" post-form: "投稿窗口"
server: "服务器信息" server: "服务器信息"
nav: "导航" nav: "导航"
tips: "提示" tips: "提示"
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
word-mute: "文字屏蔽" word-mute: "文字屏蔽"
muted-words: "屏蔽关键字" muted-words: "屏蔽关键字"
muted-words-description: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范" muted-words-description: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范"
unmute-confirm: "取消屏蔽用户?"
unblock-confirm: "取消拉黑此用户?"
save: "保存" save: "保存"
common/views/components/password-settings.vue: common/views/components/password-settings.vue:
reset: "更改密码" reset: "更改密码"
@ -1276,7 +1279,7 @@ admin/views/instance.vue:
object-storage-s3-info-here: "这里" object-storage-s3-info-here: "这里"
object-storage-gcs-info: "将Google Cloud Storage用作对象存储时请将“终端”设置为storage.googleapis.com并将“区域”留空。" object-storage-gcs-info: "将Google Cloud Storage用作对象存储时请将“终端”设置为storage.googleapis.com并将“区域”留空。"
cache-remote-files: "远程文件缓存" cache-remote-files: "远程文件缓存"
cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。" proxy-remote-files: "代理远程文件"
local-drive-capacity-mb: "每个用户的网盘空间" local-drive-capacity-mb: "每个用户的网盘空间"
remote-drive-capacity-mb: "每个远程用户的网盘容量" remote-drive-capacity-mb: "每个远程用户的网盘容量"
mb: "以兆字节(Mbps)为单位" mb: "以兆字节(Mbps)为单位"
@ -1880,10 +1883,10 @@ pages:
section: "章节" section: "章节"
image: "图片" image: "图片"
button: "按钮" button: "按钮"
if: "如果" if: "判断"
_if: _if:
variable: "变量" variable: "变量"
post: "投稿形式" post: "投稿窗口"
_post: _post:
text: "内容" text: "内容"
textInput: "文本输入" textInput: "文本输入"
@ -1989,6 +1992,9 @@ pages:
_mod: _mod:
arg1: "A" arg1: "A"
arg2: "B" arg2: "B"
round: "四舍五入"
_round:
arg1: "数值"
eq: "A和B相等" eq: "A和B相等"
_eq: _eq:
arg1: "A" arg1: "A"

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class TalkFederationId1576269851876 implements MigrationInterface {
name = 'TalkFederationId1576269851876'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "messaging_message" ADD "uri" character varying(512)`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "uri"`, undefined);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class ProxyRemoteFiles1576869585998 implements MigrationInterface {
name = 'ProxyRemoteFiles1576869585998'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT false`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`, undefined);
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.35.1", "version": "11.37.1",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,52 +26,48 @@
"format": "gulp format" "format": "gulp format"
}, },
"resolutions": { "resolutions": {
"gulp-cssnano/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1",
"https-proxy-agent": "^3.0.0", "https-proxy-agent": "^3.0.0",
"lodash": "^4.17.13" "lodash": "^4.17.13"
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "7.4.0", "@elastic/elasticsearch": "7.5.0",
"@fortawesome/fontawesome-svg-core": "1.2.25", "@fortawesome/fontawesome-svg-core": "1.2.26",
"@fortawesome/free-brands-svg-icons": "5.11.2", "@fortawesome/free-brands-svg-icons": "5.12.0",
"@fortawesome/free-regular-svg-icons": "5.11.2", "@fortawesome/free-regular-svg-icons": "5.12.0",
"@fortawesome/free-solid-svg-icons": "5.11.2", "@fortawesome/free-solid-svg-icons": "5.12.0",
"@fortawesome/vue-fontawesome": "0.1.7", "@fortawesome/vue-fontawesome": "0.1.9",
"@koa/cors": "3.0.0", "@koa/cors": "3.0.0",
"@koa/multer": "2.0.0", "@koa/multer": "2.0.2",
"@koa/router": "8.0.2", "@koa/router": "8.0.5",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.10.3", "@types/bull": "3.10.6",
"@types/cbor": "2.0.0", "@types/cbor": "5.0.0",
"@types/dateformat": "3.0.1", "@types/dateformat": "3.0.1",
"@types/deep-equal": "1.0.1",
"@types/double-ended-queue": "2.1.1", "@types/double-ended-queue": "2.1.1",
"@types/gulp": "4.0.6", "@types/gulp": "4.0.6",
"@types/gulp-mocha": "0.0.32", "@types/gulp-mocha": "0.0.32",
"@types/gulp-rename": "0.0.33", "@types/gulp-rename": "0.0.33",
"@types/gulp-replace": "0.0.31", "@types/gulp-replace": "0.0.31",
"@types/gulp-uglify": "3.0.6",
"@types/gulp-util": "3.0.34",
"@types/is-url": "1.2.28", "@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.1", "@types/js-yaml": "3.12.1",
"@types/jsdom": "12.2.4", "@types/jsdom": "12.2.4",
"@types/katex": "0.10.2", "@types/katex": "0.11.0",
"@types/koa": "2.0.50", "@types/koa": "2.11.0",
"@types/koa-bodyparser": "5.0.2", "@types/koa-bodyparser": "4.3.0",
"@types/koa-compress": "2.0.9", "@types/koa-compress": "2.0.9",
"@types/koa-cors": "0.0.0", "@types/koa-cors": "0.0.0",
"@types/koa-favicon": "2.0.19", "@types/koa-favicon": "2.0.19",
"@types/koa-logger": "3.1.1", "@types/koa-logger": "3.1.1",
"@types/koa-mount": "4.0.0", "@types/koa-mount": "4.0.0",
"@types/koa-send": "4.1.2", "@types/koa-send": "4.1.2",
"@types/koa-views": "2.0.3", "@types/koa-views": "2.0.4",
"@types/koa__cors": "2.2.3", "@types/koa__cors": "3.0.0",
"@types/koa__multer": "2.0.0", "@types/koa__multer": "2.0.1",
"@types/koa__router": "8.0.0", "@types/koa__router": "8.0.2",
"@types/lolex": "3.1.1", "@types/lolex": "5.1.0",
"@types/mocha": "5.2.7", "@types/mocha": "5.2.7",
"@types/node": "12.7.12", "@types/node": "13.1.4",
"@types/nodemailer": "6.2.1", "@types/nodemailer": "6.4.0",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/parse5": "5.0.2", "@types/parse5": "5.0.2",
@ -83,80 +79,78 @@
"@types/ratelimiter": "2.1.28", "@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.14", "@types/redis": "2.8.14",
"@types/rename": "1.0.1", "@types/rename": "1.0.1",
"@types/request": "2.48.3", "@types/request": "2.48.4",
"@types/request-promise-native": "1.0.17", "@types/request-promise-native": "1.0.17",
"@types/request-stats": "3.0.0", "@types/request-stats": "3.0.0",
"@types/rimraf": "2.0.2", "@types/rimraf": "2.0.3",
"@types/seedrandom": "2.4.28", "@types/seedrandom": "2.4.28",
"@types/sharp": "0.22.3", "@types/sharp": "0.23.1",
"@types/showdown": "1.9.3", "@types/showdown": "1.9.3",
"@types/speakeasy": "2.0.5", "@types/speakeasy": "2.0.5",
"@types/systeminformation": "3.23.1", "@types/systeminformation": "3.54.1",
"@types/tinycolor2": "1.4.2", "@types/tinycolor2": "1.4.2",
"@types/tmp": "0.1.0", "@types/tmp": "0.1.0",
"@types/uuid": "3.4.5", "@types/uuid": "3.4.6",
"@types/web-push": "3.3.0", "@types/web-push": "3.3.0",
"@types/webpack": "4.39.3", "@types/webpack": "4.41.1",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.40", "@types/websocket": "1.0.0",
"@types/ws": "6.0.3", "@types/ws": "6.0.4",
"@typescript-eslint/parser": "2.3.3", "@typescript-eslint/parser": "2.15.0",
"agentkeepalive": "4.1.0", "agentkeepalive": "4.1.0",
"animejs": "3.1.0", "animejs": "3.1.0",
"apexcharts": "3.10.1", "apexcharts": "3.12.0",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.548.0", "aws-sdk": "2.598.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bootstrap": "4.3.1", "bootstrap": "4.4.1",
"bootstrap-vue": "2.0.4", "bootstrap-vue": "2.1.0",
"bull": "3.11.0", "bull": "3.12.1",
"cafy": "15.1.1", "cafy": "15.2.1",
"cbor": "5.0.1", "cbor": "5.0.1",
"chai": "4.2.0", "chai": "4.2.0",
"chalk": "2.4.2", "chalk": "3.0.0",
"cli-highlight": "2.1.1", "cli-highlight": "2.1.4",
"commander": "3.0.2", "commander": "4.1.0",
"content-disposition": "0.5.3", "content-disposition": "0.5.3",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "3.2.0", "css-loader": "3.4.1",
"cssnano": "4.1.10", "cssnano": "4.1.10",
"dateformat": "3.0.3", "dateformat": "3.0.3",
"deep-equal": "1.1.0",
"diskusage": "1.1.3", "diskusage": "1.1.3",
"double-ended-queue": "2.1.0-0", "double-ended-queue": "2.1.0-0",
"eslint": "6.5.1", "eslint": "6.8.0",
"eslint-plugin-vue": "5.2.3", "eslint-plugin-vue": "6.1.2",
"eventemitter3": "4.0.0", "eventemitter3": "4.0.0",
"feed": "4.0.0", "feed": "4.1.0",
"file-type": "12.3.0", "file-type": "13.0.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-clean-css": "4.2.0",
"gulp-mocha": "7.0.2", "gulp-mocha": "7.0.2",
"gulp-rename": "1.4.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.5", "gulp-sourcemaps": "2.6.5",
"gulp-stylus": "2.7.0", "gulp-stylus": "2.7.0",
"gulp-terser": "1.2.0",
"gulp-tslint": "8.1.4", "gulp-tslint": "8.1.4",
"gulp-typescript": "5.0.1", "gulp-typescript": "5.0.1",
"gulp-uglify": "3.0.2",
"gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.13.1", "hard-source-webpack-plugin": "0.13.1",
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
"http-signature": "1.2.0", "http-signature": "1.3.1",
"https-proxy-agent": "3.0.0", "https-proxy-agent": "4.0.0",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0", "is-root": "2.1.0",
"is-svg": "4.2.0", "is-svg": "4.2.0",
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"jsdom": "15.1.1", "jsdom": "15.2.1",
"json5": "2.1.1", "json5": "2.1.1",
"json5-loader": "3.0.0", "json5-loader": "3.0.0",
"jsrsasign": "8.0.12", "jsrsasign": "8.0.12",
"katex": "0.11.1", "katex": "0.11.1",
"koa": "2.10.0", "koa": "2.11.0",
"koa-bodyparser": "4.2.1", "koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0", "koa-compress": "3.0.0",
"koa-favicon": "2.0.1", "koa-favicon": "2.0.1",
@ -168,34 +162,34 @@
"koa-views": "6.2.1", "koa-views": "6.2.1",
"langmap": "0.0.16", "langmap": "0.0.16",
"loader-utils": "1.2.3", "loader-utils": "1.2.3",
"lolex": "4.2.0", "lolex": "5.1.2",
"lookup-dns-cache": "2.1.0", "lookup-dns-cache": "2.1.0",
"mocha": "6.2.1", "mocha": "7.0.0",
"moji": "0.5.1", "moji": "0.5.1",
"ms": "2.1.2", "ms": "2.1.2",
"multer": "1.4.2", "multer": "1.4.2",
"nested-property": "1.0.1", "nested-property": "1.0.2",
"node-fetch": "2.6.0", "node-fetch": "2.6.0",
"nodemailer": "6.3.1", "nodemailer": "6.4.2",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "5.1.0", "parse5": "5.1.1",
"parsimmon": "1.13.0", "parsimmon": "1.13.0",
"pg": "7.12.1", "pg": "7.17.0",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"prismjs": "1.17.1", "prismjs": "1.18.0",
"progress-bar-webpack-plugin": "1.12.1", "progress-bar-webpack-plugin": "1.12.1",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"promise-sequential": "1.1.1", "promise-sequential": "1.1.1",
"pug": "2.0.4", "pug": "2.0.4",
"punycode": "2.1.1", "punycode": "2.1.1",
"pureimage": "0.1.6", "pureimage": "0.1.6",
"qrcode": "1.4.2", "qrcode": "1.4.4",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"randomcolor": "0.5.4", "randomcolor": "0.5.4",
"ratelimiter": "3.3.1", "ratelimiter": "3.4.0",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.2.0", "reconnecting-websocket": "4.2.0",
"redis": "2.8.0", "redis": "2.8.0",
@ -203,69 +197,68 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"request": "2.88.0", "request": "2.88.0",
"request-promise-native": "1.0.7", "request-promise-native": "1.0.8",
"request-stats": "3.0.0", "request-stats": "3.0.0",
"require-all": "3.0.0", "require-all": "3.0.0",
"rimraf": "3.0.0", "rimraf": "3.0.0",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"sharp": "0.23.1", "sharp": "0.23.4",
"showdown": "1.9.0", "showdown": "1.9.1",
"showdown-highlightjs-extension": "0.1.2", "showdown-highlightjs-extension": "0.1.2",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"stringz": "2.0.0", "stringz": "2.0.0",
"style-loader": "1.0.0", "style-loader": "1.1.2",
"stylus": "0.54.7", "stylus": "0.54.7",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.3.1", "summaly": "2.3.1",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "4.14.11", "systeminformation": "4.17.3",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "2.1.3", "terser-webpack-plugin": "2.3.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.109.0", "three": "0.112.1",
"tinycolor2": "1.4.1", "tinycolor2": "1.4.1",
"tmp": "0.1.0", "tmp": "0.1.0",
"ts-loader": "6.2.0", "ts-loader": "6.2.1",
"ts-node": "8.4.1", "ts-node": "8.5.4",
"tslint": "5.20.0", "tslint": "5.20.1",
"tslint-sonarts": "1.9.0", "tslint-sonarts": "1.9.0",
"typeorm": "0.2.19", "typeorm": "0.2.22",
"typescript": "3.6.4", "typescript": "3.7.4",
"uglify-es": "3.3.9",
"ulid": "2.3.0", "ulid": "2.3.0",
"url-loader": "2.2.0", "url-loader": "3.0.0",
"uuid": "3.3.3", "uuid": "3.3.3",
"v-animate-css": "0.0.3", "v-animate-css": "0.0.3",
"v-debounce": "0.1.2", "v-debounce": "0.1.2",
"vue": "2.6.10", "vue": "2.6.11",
"vue-color": "2.7.0", "vue-color": "2.7.0",
"vue-content-loading": "1.6.0", "vue-content-loading": "1.6.0",
"vue-cropperjs": "4.0.0", "vue-cropperjs": "4.0.1",
"vue-i18n": "8.14.1", "vue-i18n": "8.15.3",
"vue-js-modal": "1.3.31", "vue-js-modal": "1.3.31",
"vue-json-pretty": "1.6.2", "vue-json-pretty": "1.6.3",
"vue-loader": "15.7.1", "vue-loader": "15.8.3",
"vue-marquee-text-component": "1.1.1", "vue-marquee-text-component": "1.1.1",
"vue-prism-component": "1.1.1", "vue-prism-component": "1.1.1",
"vue-router": "3.1.3", "vue-router": "3.1.3",
"vue-sequential-entrance": "1.1.3", "vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.3.3", "vue-svg-inline-loader": "1.4.4",
"vue-template-compiler": "2.6.10", "vue-template-compiler": "2.6.11",
"vuedraggable": "2.23.2", "vuedraggable": "2.23.2",
"vuewordcloud": "18.7.11", "vuewordcloud": "18.7.11",
"vuex": "3.1.1", "vuex": "3.1.2",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.7.0",
"web-push": "3.4.0", "web-push": "3.4.3",
"webpack": "4.41.1", "webpack": "4.41.5",
"webpack-cli": "3.3.9", "webpack-cli": "3.3.10",
"websocket": "1.0.30", "websocket": "1.0.31",
"ws": "7.1.2", "ws": "7.2.1",
"xev": "2.0.1" "xev": "2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fluent-ffmpeg": "2.1.10" "@types/fluent-ffmpeg": "2.1.12"
} }
} }

View File

@ -1,5 +1,5 @@
import * as cluster from 'cluster'; import * as cluster from 'cluster';
import chalk from 'chalk'; import * as chalk from 'chalk';
import Xev from 'xev'; import Xev from 'xev';
import Logger from '../services/logger'; import Logger from '../services/logger';

View File

@ -1,6 +1,6 @@
import * as os from 'os'; import * as os from 'os';
import * as cluster from 'cluster'; import * as cluster from 'cluster';
import chalk from 'chalk'; import * as chalk from 'chalk';
import * as portscanner from 'portscanner'; import * as portscanner from 'portscanner';
import * as isRoot from 'is-root'; import * as isRoot from 'is-root';
@ -11,14 +11,15 @@ import { lessThan } from '../prelude/array';
import { program } from '../argv'; import { program } from '../argv';
import { showMachineInfo } from '../misc/show-machine-info'; import { showMachineInfo } from '../misc/show-machine-info';
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
import * as meta from '../meta.json';
const logger = new Logger('core', 'cyan'); const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false); const bootLogger = logger.createSubLogger('boot', 'magenta', false);
function greet(config: Config) { function greet() {
if (!program.quiet) { if (!program.quiet) {
//#region Misskey logo //#region Misskey logo
const v = `v${config.version}`; const v = `v${meta.version}`;
console.log(' _____ _ _ '); console.log(' _____ _ _ ');
console.log(' | |_|___ ___| |_ ___ _ _ '); console.log(' | |_|___ ___| |_ ___ _ _ ');
console.log(' | | | | |_ -|_ -| \'_| -_| | |'); console.log(' | | | | |_ -|_ -| \'_| -_| | |');
@ -34,7 +35,7 @@ function greet(config: Config) {
} }
bootLogger.info('Welcome to Misskey!'); bootLogger.info('Welcome to Misskey!');
bootLogger.info(`Misskey v${config.version}`, null, true); bootLogger.info(`Misskey v${meta.version}`, null, true);
} }
/** /**
@ -44,11 +45,11 @@ export async function masterMain() {
let config!: Config; let config!: Config;
try { try {
greet();
// initialize app // initialize app
config = await init(); config = await init();
greet(config);
if (config.port == null || Number.isNaN(config.port)) { if (config.port == null || Number.isNaN(config.port)) {
bootLogger.error('The port is not configured. Please configure port.', null, true); bootLogger.error('The port is not configured. Please configure port.', null, true);
process.exit(1); process.exit(1);

View File

@ -115,7 +115,6 @@ export default Vue.extend({
connection: null, connection: null,
meta: null, meta: null,
instances: [], instances: [],
clock: null,
faDatabase faDatabase
}; };
}, },
@ -124,7 +123,6 @@ export default Vue.extend({
this.connection = this.$root.stream.useSharedConnection('serverStats'); this.connection = this.$root.stream.useSharedConnection('serverStats');
this.updateStats(); this.updateStats();
this.clock = setInterval(this.updateStats, 3000);
this.$root.getMeta().then(meta => { this.$root.getMeta().then(meta => {
this.meta = meta; this.meta = meta;
@ -145,7 +143,6 @@ export default Vue.extend({
beforeDestroy() { beforeDestroy() {
this.connection.dispose(); this.connection.dispose();
clearInterval(this.clock);
}, },
methods: { methods: {

View File

@ -81,6 +81,7 @@
</section> </section>
<section> <section>
<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch>
<ui-switch v-model="proxyRemoteFiles">{{ $t('proxy-remote-files') }}<template #desc>{{ $t('proxy-remote-files-desc') }}</template></ui-switch>
</section> </section>
<section class="fit-top fit-bottom"> <section class="fit-top fit-bottom">
<ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
@ -275,6 +276,7 @@ export default Vue.extend({
description: null, description: null,
languages: null, languages: null,
cacheRemoteFiles: false, cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: null, localDriveCapacityMb: null,
remoteDriveCapacityMb: null, remoteDriveCapacityMb: null,
maxNoteTextLength: null, maxNoteTextLength: null,
@ -339,6 +341,7 @@ export default Vue.extend({
this.description = meta.description; this.description = meta.description;
this.languages = meta.langs.join(' '); this.languages = meta.langs.join(' ');
this.cacheRemoteFiles = meta.cacheRemoteFiles; this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.proxyRemoteFiles = meta.proxyRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
this.maxNoteTextLength = meta.maxNoteTextLength; this.maxNoteTextLength = meta.maxNoteTextLength;
@ -463,6 +466,7 @@ export default Vue.extend({
description: this.description, description: this.description,
langs: this.languages ? this.languages.split(' ') : [], langs: this.languages ? this.languages.split(' ') : [],
cacheRemoteFiles: this.cacheRemoteFiles, cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
maxNoteTextLength: parseInt(this.maxNoteTextLength, 10), maxNoteTextLength: parseInt(this.maxNoteTextLength, 10),

View File

@ -48,14 +48,15 @@
</ui-select> </ui-select>
</ui-horizon-group> </ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25"> <sequential-entrance animation="entranceFromTop" delay="25">
<div class="xvvuvgsv" v-for="job in jobs"> <div class="xvvuvgsv" v-for="job in jobs" :key="job.id">
<b>{{ job.id }}</b> <b>{{ job.id }}</b>
<template v-if="domain === 'deliver'"> <template v-if="domain === 'deliver'">
<span>{{ job.data.to }}</span> <span>{{ job.data.to }}</span>
</template> </template>
<template v-if="domain === 'inbox'"> <template v-if="domain === 'inbox'">
<span>{{ job.activity.id }}</span> <span>{{ job.data.activity.id }}</span>
</template> </template>
<span>{{ `(${job.attempts}/${job.maxAttempts}, ${Math.floor((jobsFetched - job.timestamp) / 1000 / 60)}min)` }}</span>
</div> </div>
</sequential-entrance> </sequential-entrance>
<ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info> <ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info>
@ -84,6 +85,7 @@ export default Vue.extend({
chartLimit: 200, chartLimit: 200,
jobs: [], jobs: [],
jobsLimit: 50, jobsLimit: 50,
jobsFetched: Date.now(),
domain: 'deliver', domain: 'deliver',
state: 'delayed', state: 'delayed',
faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud
@ -140,6 +142,7 @@ export default Vue.extend({
state: this.state, state: this.state,
limit: this.jobsLimit limit: this.jobsLimit
}).then(jobs => { }).then(jobs => {
this.jobsFetched = Date.now(),
this.jobs = jobs; this.jobs = jobs;
}); });
}, },
@ -149,7 +152,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.xvvuvgsv .xvvuvgsv
> b margin-left -6px
margin-right 16px > b, span
margin 0 6px
</style> </style>

View File

@ -72,13 +72,17 @@
//#region Fetch locale data //#region Fetch locale data
const cachedLocale = localStorage.getItem('locale'); const cachedLocale = localStorage.getItem('locale');
const localeKey = localStorage.getItem('localeKey'); const localeKey = localStorage.getItem('localeKey');
let localeData = null;
if (cachedLocale == null || localeKey != `${ver}.${lang}`) { if (cachedLocale == null || localeKey != `${ver}.${lang}`) {
const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`) const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`)
.then(response => response.json()); .then(response => response.json());
localeData = locale;
localStorage.setItem('locale', JSON.stringify(locale)); localStorage.setItem('locale', JSON.stringify(locale));
localStorage.setItem('localeKey', `${ver}.${lang}`); localStorage.setItem('localeKey', `${ver}.${lang}`);
} else {
localeData = JSON.parse(cachedLocale);
} }
//#endregion //#endregion
@ -99,8 +103,7 @@
// If mobile, insert the viewport meta tag // If mobile, insert the viewport meta tag
if (isMobile) { if (isMobile) {
const viewport = document.getElementsByName("viewport").item(0); const viewport = document.getElementsByName("viewport").item(0);
viewport.setAttribute('content', viewport.content = `${viewport.content},minimum-scale=1,maximum-scale=1,user-scalable=no`;
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
head.appendChild(viewport); head.appendChild(viewport);
} }
@ -113,9 +116,9 @@
// Note: 'async' make it possible to load the script asyncly. // Note: 'async' make it possible to load the script asyncly.
// 'defer' make it possible to run the script when the dom loaded. // 'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script'); const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${ver}.js`); script.src = `/assets/${app}.${ver}.js`;
script.setAttribute('async', 'true'); script.async = true;
script.setAttribute('defer', 'true'); script.defer = true;
head.appendChild(script); head.appendChild(script);
// 3秒経ってもスクリプトがロードされない場合はバージョンが古くて // 3秒経ってもスクリプトがロードされない場合はバージョンが古くて
@ -138,10 +141,10 @@
localStorage.setItem('v', meta.version); localStorage.setItem('v', meta.version);
alert( alert(
'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + localeData.common._settings["update-available"] +
'\n\n' + '\n' +
'New version of Misskey available. The page will be reloaded.'); localeData.common._settings["update-available-desc"]
);
refresh(); refresh();
} }
}, 3000); }, 3000);

View File

@ -42,7 +42,7 @@ export default Vue.extend({
}, },
methods: { methods: {
previewable(file) { previewable(file) {
return file.type.startsWith('video') || file.type.startsWith('image'); return (file.type.startsWith('video') || file.type.startsWith('image')) && file.thumbnailUrl;
} }
} }
}); });

View File

@ -0,0 +1,39 @@
<template>
<div class="muteblockuser">
<div class="avatar-link">
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div class="text">
<div><mk-user-name :user="user"/></div>
<div class="username">@{{ user | acct }}</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/mute-and-block.user.vue'),
props: ['user'],
});
</script>
<style lang="stylus" scoped>
.muteblockuser
display flex
padding 16px
> .avatar-link
> a
> .avatar
width 40px
height 40px
> .text
color var(--text)
margin-left 16px
</style>

View File

@ -6,9 +6,13 @@
<header>{{ $t('mute') }}</header> <header>{{ $t('mute') }}</header>
<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info> <ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
<div class="users" v-if="mute.length != 0"> <div class="users" v-if="mute.length != 0">
<div v-for="user in mute" :key="user.id"> <div class="user" v-for="user in mute" :key="user.id">
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p> <x-user :user="user"/>
<span @click="unmute(user)">
<fa icon="times"/>
</span>
</div> </div>
<ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button>
</div> </div>
</section> </section>
@ -16,9 +20,13 @@
<header>{{ $t('block') }}</header> <header>{{ $t('block') }}</header>
<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info> <ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
<div class="users" v-if="block.length != 0"> <div class="users" v-if="block.length != 0">
<div v-for="user in block" :key="user.id"> <div class="user" v-for="user in block" :key="user.id">
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p> <x-user :user="user"/>
<span @click="unblock(user)">
<fa icon="times"/>
</span>
</div> </div>
<ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button>
</div> </div>
</section> </section>
@ -35,16 +43,25 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../../i18n'; import i18n from '../../../../i18n';
import XUser from './mute-and-block.user.vue';
const fetchLimit = 30;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/mute-and-block.vue'), i18n: i18n('common/views/components/mute-and-block.vue'),
components: {
XUser
},
data() { data() {
return { return {
muteFetching: true, muteFetching: true,
blockFetching: true, blockFetching: true,
mute: [], mute: [],
block: [], block: [],
muteCursor: undefined,
blockCursor: undefined,
mutedWords: '' mutedWords: ''
}; };
}, },
@ -59,21 +76,106 @@ export default Vue.extend({
mounted() { mounted() {
this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n'); this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n');
this.$root.api('mute/list').then(mute => { this.updateMute();
this.mute = mute.map(x => x.mutee); this.updateBlock();
this.muteFetching = false;
});
this.$root.api('blocking/list').then(blocking => {
this.block = blocking.map(x => x.blockee);
this.blockFetching = false;
});
}, },
methods: { methods: {
save() { save() {
this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != '')); this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != ''));
},
unmute(user) {
this.$root.dialog({
type: 'warning',
text: this.$t('unmute-confirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('mute/delete', {
userId: user.id
}).then(() => {
this.muteCursor = undefined;
this.updateMute();
});
});
},
unblock(user) {
this.$root.dialog({
type: 'warning',
text: this.$t('unblock-confirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('blocking/delete', {
userId: user.id
}).then(() => {
this.updateBlock();
});
});
},
updateMute() {
this.muteFetching = true;
this.$root.api('mute/list', {
limit: fetchLimit + 1,
untilId: this.muteCursor,
}).then((items: Object[]) => {
const past = this.muteCursor ? this.mute : [];
if (items.length === fetchLimit + 1) {
items.pop()
this.muteCursor = items[items.length - 1].id;
} else {
this.muteCursor = undefined;
} }
this.mute = past.concat(items.map(x => x.mutee));
this.muteFetching = false;
});
},
updateBlock() {
this.blockFetching = true;
this.$root.api('blocking/list', {
limit: fetchLimit + 1,
untilId: this.blockCursor,
}).then((items: Object[]) => {
const past = this.blockCursor ? this.block : [];
if (items.length === fetchLimit + 1) {
items.pop()
this.blockCursor = items[items.length - 1].id;
} else {
this.blockCursor = undefined;
}
this.block = past.concat(items.map(x => x.blockee));
this.blockFetching = false;
});
},
} }
}); });
</script> </script>
<style lang="stylus" scoped>
.users
> .user
display flex
align-items center
justify-content flex-end
border-radius 6px
&:hover
background-color var(--primary)
> span
margin-left auto
cursor pointer
padding 16px
> button
margin-top 16px
</style>

View File

@ -76,13 +76,15 @@ export default Vue.extend({
this.$root.api('ap/show', { this.$root.api('ap/show', {
uri: acct uri: acct
}).then((res: { type: string, object: any }) => { }).then((res: { type: string, object: any }) => {
if (res.type !== 'User') { if (res.type === 'User') {
this.user = res.object;
} else if (res.type === 'Note') {
this.$router.replace(`/notes/${res.object.id}`);
} else {
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
text: 'acct is not an user' text: 'Not supported'
}); });
} else {
this.user = res.object;
} }
}).catch((e: any) => { }).catch((e: any) => {
this.$root.dialog({ this.$root.dialog({

View File

@ -43,6 +43,8 @@ export default function load() {
if (config.autoAdmin == null) config.autoAdmin = false; if (config.autoAdmin == null) config.autoAdmin = false;
if (!config.redis.prefix) config.redis.prefix = mixin.host;
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }

View File

@ -52,6 +52,8 @@ export type Source = {
host: string; host: string;
port: number; port: number;
}; };
mediaProxy?: string;
}; };
/** /**

View File

@ -1,3 +1,3 @@
{ {
"copyright": "Copyright (c) 2014-2019 syuilo" "copyright": "Copyright (c) 2014-2020 syuilo"
} }

View File

@ -155,7 +155,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
host: config.redis.host, host: config.redis.host,
port: config.redis.port, port: config.redis.port,
password: config.redis.pass, password: config.redis.pass,
prefix: config.redis.prefix, prefix: `${config.redis.prefix}:query:`,
db: config.redis.db || 0 db: config.redis.db || 0
} }
} : false, } : false,

View File

@ -162,6 +162,7 @@ export class ASEvaluator {
multiply: (a: number, b: number) => a * b, multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b, divide: (a: number, b: number) => a / b,
mod: (a: number, b: number) => a % b, mod: (a: number, b: number) => a % b,
round: (a: number) => Math.round(a),
strLen: (a: string) => a.length, strLen: (a: string) => a.length,
strPick: (a: string, b: number) => a[b - 1], strPick: (a: string, b: number) => a[b - 1],
strReplace: (a: string, b: string, c: string) => a.split(b).join(c), strReplace: (a: string, b: string, c: string) => a.split(b).join(c),

View File

@ -24,6 +24,7 @@ import {
faExchangeAlt, faExchangeAlt,
faRecycle, faRecycle,
faIndent, faIndent,
faCalculator,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons'; import { faFlag } from '@fortawesome/free-regular-svg-icons';
@ -59,6 +60,7 @@ export const funcDefs: Record<string, { in: any[]; out: any; category: string; i
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, }, multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, },
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, }, divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, }, mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
round: { in: ['number'], out: 'number', category: 'operation', icon: faCalculator, },
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, }, eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, },
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, }, notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, }, gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },

View File

@ -1,15 +1,15 @@
import * as fs from 'fs'; import * as fs from 'fs';
import fileType = require('file-type');
import checkSvg from '../misc/check-svg'; import checkSvg from '../misc/check-svg';
const FileType = require('file-type');
export async function detectMine(path: string) { export async function detectMine(path: string) {
return new Promise<[string, string | null]>((res, rej) => { return new Promise<[string, string | null]>((res, rej) => {
const readable = fs.createReadStream(path); const readable = fs.createReadStream(path);
readable readable
.on('error', rej) .on('error', rej)
.once('data', (buffer: Buffer) => { .once('data', async (buffer: Buffer) => {
readable.destroy(); readable.destroy();
const type = fileType(buffer); const type = await FileType.fromBuffer(buffer);
if (type) { if (type) {
if (type.mime == 'application/xml' && checkSvg(path)) { if (type.mime == 'application/xml' && checkSvg(path)) {
res(['image/svg+xml', 'svg']); res(['image/svg+xml', 'svg']);

View File

@ -1,7 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as request from 'request'; import * as request from 'request';
import config from '../config'; import config from '../config';
import chalk from 'chalk'; import * as chalk from 'chalk';
import Logger from '../services/logger'; import Logger from '../services/logger';
export async function downloadUrl(url: string, path: string) { export async function downloadUrl(url: string, path: string) {

View File

@ -64,6 +64,11 @@ export class MessagingMessage {
}) })
public isRead: boolean; public isRead: boolean;
@Column('varchar', {
length: 512, nullable: true,
})
public uri: string | null;
@Column({ @Column({
...id(), ...id(),
array: true, default: '{}' array: true, default: '{}'

View File

@ -115,6 +115,11 @@ export class Meta {
}) })
public cacheRemoteFiles: boolean; public cacheRemoteFiles: boolean;
@Column('boolean', {
default: false,
})
public proxyRemoteFiles: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
nullable: true nullable: true

View File

@ -6,6 +6,10 @@ import { toPuny } from '../../misc/convert-host';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all'; import { awaitAll } from '../../prelude/await-all';
import { SchemaType } from '../../misc/schema'; import { SchemaType } from '../../misc/schema';
import config from '../../config';
import { query, appendQuery } from '../../prelude/url';
import { Meta } from '../entities/meta';
import { fetchMeta } from '../../misc/fetch-meta';
export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>; export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
@ -21,8 +25,27 @@ export class DriveFileRepository extends Repository<DriveFile> {
); );
} }
public getPublicUrl(file: DriveFile, thumbnail = false): string | null { public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
return thumbnail ? (file.thumbnailUrl || file.webpublicUrl || null) : (file.webpublicUrl || file.url); // リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
return appendQuery(config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? '1' : undefined
}));
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && meta && meta.proxyRemoteFiles) {
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
return `/files/${key}`;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
} }
public async clacDriveUsageOf(user: User['id'] | User): Promise<number> { public async clacDriveUsageOf(user: User['id'] | User): Promise<number> {
@ -82,6 +105,8 @@ export class DriveFileRepository extends Repository<DriveFile> {
const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure); const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const meta = await fetchMeta();
return await awaitAll({ return await awaitAll({
id: file.id, id: file.id,
createdAt: file.createdAt.toISOString(), createdAt: file.createdAt.toISOString(),
@ -91,8 +116,8 @@ export class DriveFileRepository extends Repository<DriveFile> {
size: file.size, size: file.size,
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
properties: file.properties, properties: file.properties,
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true), thumbnailUrl: this.getPublicUrl(file, true, meta),
folderId: file.folderId, folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true detail: true

View File

@ -129,6 +129,31 @@ export class NoteRepository extends Repository<Note> {
}; };
} }
async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
const where = [] as {}[];
if (emojiNames?.length > 0) {
where.push({
name: In(emojiNames),
host: noteUserHost
});
}
if (reactionNames?.length > 0) {
where.push({
name: In(reactionNames.map(x => x.replace(/:/g, ''))),
host: null
});
}
if (where.length === 0) return [];
return Emojis.find({
where,
select: ['name', 'host', 'url', 'aliases']
});
}
async function populateMyReaction() { async function populateMyReaction() {
const reaction = await NoteReactions.findOne({ const reaction = await NoteReactions.findOne({
userId: meId!, userId: meId!,
@ -148,8 +173,6 @@ export class NoteRepository extends Repository<Note> {
text = `${note.name}\n${(note.text || '').trim()}\n${note.uri}`; text = `${note.name}\n${(note.text || '').trim()}\n${note.uri}`;
} }
const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
const packed = await awaitAll({ const packed = await awaitAll({
id: note.id, id: note.id,
createdAt: note.createdAt.toISOString(), createdAt: note.createdAt.toISOString(),
@ -166,10 +189,7 @@ export class NoteRepository extends Repository<Note> {
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: note.reactions, reactions: note.reactions,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
emojis: reactionEmojis.length > 0 ? Emojis.find({ emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)),
name: In(reactionEmojis),
host: host
}) : [],
fileIds: note.fileIds, fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds), files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId, replyId: note.replyId,

15
src/queue/get-job-info.ts Normal file
View File

@ -0,0 +1,15 @@
import * as Bull from 'bull';
export function getJobInfo(job: Bull.Job, increment = false) {
const age = Date.now() - job.timestamp;
const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
: age > 10000 ? `${Math.floor(age / 1000)}s`
: `${age}ms`;
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
const maxAttempts = job.opts ? job.opts.attempts : 0;
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
}

View File

@ -11,6 +11,7 @@ import processDb from './processors/db';
import procesObjectStorage from './processors/object-storage'; import procesObjectStorage from './processors/object-storage';
import { queueLogger } from './logger'; import { queueLogger } from './logger';
import { DriveFile } from '../models/entities/drive-file'; import { DriveFile } from '../models/entities/drive-file';
import { getJobInfo } from './get-job-info';
function initializeQueue(name: string) { function initializeQueue(name: string) {
return new Queue(name, { return new Queue(name, {
@ -44,19 +45,19 @@ const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
deliverQueue deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`, { job, e: renderError(err) })) .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`)); .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
inboxQueue inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
dbQueue dbQueue
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))

View File

@ -30,7 +30,12 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
const csv = await downloadTextFile(file.url); const csv = await downloadTextFile(file.url);
let linenum = 0;
for (const line of csv.trim().split('\n')) { for (const line of csv.trim().split('\n')) {
linenum++;
try {
const listName = line.split(',')[0].trim(); const listName = line.split(',')[0].trim();
const { username, host } = parseAcct(line.split(',')[1].trim()); const { username, host } = parseAcct(line.split(',')[1].trim());
@ -64,6 +69,9 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue; if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
pushUserToUserList(target, list); pushUserToUserList(target, list);
} catch (e) {
logger.warn(`Error in line:${linenum} ${e}`);
}
} }
logger.succ('Imported'); logger.succ('Imported');

View File

@ -5,6 +5,8 @@ import Logger from '../../services/logger';
import { Instances } from '../../models'; import { Instances } from '../../models';
import { instanceChart } from '../../services/chart'; import { instanceChart } from '../../services/chart';
import { fetchNodeinfo } from '../../services/fetch-nodeinfo'; import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
import { fetchMeta } from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host';
const logger = new Logger('deliver'); const logger = new Logger('deliver');
@ -13,6 +15,23 @@ let latest: string | null = null;
export default async (job: Bull.Job) => { export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to); const { host } = new URL(job.data.to);
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(toPuny(host))) {
return 'skip (blocked)';
}
// closedなら中断
const closedHosts = await Instances.find({
where: {
isMarkedAsClosed: true
},
cache: 60 * 1000
});
if (closedHosts.map(x => x.host).includes(toPuny(host))) {
return 'skip (closed)';
}
try { try {
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) { if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
logger.debug(`delivering ${latest}`); logger.debug(`delivering ${latest}`);
@ -48,8 +67,6 @@ export default async (job: Bull.Job) => {
}); });
if (res != null && res.hasOwnProperty('statusCode')) { if (res != null && res.hasOwnProperty('statusCode')) {
logger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
// 4xx // 4xx
if (res.statusCode >= 400 && res.statusCode < 500) { if (res.statusCode >= 400 && res.statusCode < 500) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
@ -61,7 +78,6 @@ export default async (job: Bull.Job) => {
throw `${res.statusCode} ${res.statusMessage}`; throw `${res.statusCode} ${res.statusMessage}`;
} else { } else {
// DNS error, socket error, timeout ... // DNS error, socket error, timeout ...
logger.warn(`deliver failed: ${res} to=${job.data.to}`);
throw res; throw res;
} }
} }

View File

@ -0,0 +1,131 @@
import { Users, Followings } from '../../models';
import { ILocalUser, IRemoteUser } from '../../models/entities/user';
import { deliver } from '../../queue';
//#region types
interface IRecipe {
type: string;
}
interface IFollowersRecipe extends IRecipe {
type: 'Followers';
}
interface IDirectRecipe extends IRecipe {
type: 'Direct';
to: IRemoteUser;
}
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
recipe.type === 'Followers';
const isDirect = (recipe: any): recipe is IDirectRecipe =>
recipe.type === 'Direct';
//#endregion
export default class DeliverManager {
private actor: ILocalUser;
private activity: any;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(actor: ILocalUser, activity: any) {
this.actor = actor;
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
public addFollowersRecipe() {
const deliver = {
type: 'Followers'
} as IFollowersRecipe;
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
public addDirectRecipe(to: IRemoteUser) {
const recipe = {
type: 'Direct',
to
} as IDirectRecipe;
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
public addRecipe(recipe: IRecipe) {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
public async execute() {
if (!Users.isLocalUser(this.actor)) return;
const inboxes: string[] = [];
// build inbox list
for (const recipe of this.recipes) {
if (isFollowers(recipe)) {
// followers deliver
const followers = await Followings.find({
followeeId: this.actor.id
});
for (const following of followers) {
if (Followings.isRemoteFollower(following)) {
const inbox = following.followerSharedInbox || following.followerInbox;
if (!inboxes.includes(inbox)) inboxes.push(inbox);
}
}
} else if (isDirect(recipe)) {
// direct deliver
const inbox = recipe.to.inbox;
if (inbox && !inboxes.includes(inbox)) inboxes.push(inbox);
}
}
// deliver
for (const inbox of inboxes) {
deliver(this.actor, this.activity, inbox);
}
}
}
//#region Utilities
/**
* Deliver activity to followers
* @param activity Activity
* @param from Followee
*/
export async function deliverToFollowers(actor: ILocalUser, activity: any) {
const manager = new DeliverManager(actor, activity);
manager.addFollowersRecipe();
await manager.execute();
}
/**
* Deliver activity to user
* @param activity Activity
* @param to Target user
*/
export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) {
const manager = new DeliverManager(actor, activity);
manager.addDirectRecipe(to);
await manager.execute();
}
//#endregion

View File

@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
const resolver = new Resolver(); const resolver = new Resolver();
let object; const object = await resolver.resolve(activity.object).catch(e => {
try {
object = await resolver.resolve(activity.object);
} catch (e) {
logger.error(`Resolution failed: ${e}`); logger.error(`Resolution failed: ${e}`);
throw e; throw e;
} });
switch (object.type) { switch (object.type) {
case 'Follow': case 'Follow':

View File

@ -1,7 +1,7 @@
import Resolver from '../../resolver'; import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user'; import { IRemoteUser } from '../../../../models/entities/user';
import announceNote from './note'; import announceNote from './note';
import { IAnnounce, validPost, getApId } from '../../type'; import { IAnnounce, getApId } from '../../type';
import { apLogger } from '../../logger'; import { apLogger } from '../../logger';
const logger = apLogger; const logger = apLogger;
@ -13,18 +13,7 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
const resolver = new Resolver(); const resolver = new Resolver();
let object; const targetUri = getApId(activity.object);
try { announceNote(resolver, actor, activity, targetUri);
object = await resolver.resolve(activity.object);
} catch (e) {
logger.error(`Resolution failed: ${e}`);
throw e;
}
if (validPost.includes(object.type)) {
announceNote(resolver, actor, activity, object);
} else {
logger.warn(`Unknown announce type: ${object.type}`);
}
}; };

View File

@ -1,7 +1,7 @@
import Resolver from '../../resolver'; import Resolver from '../../resolver';
import post from '../../../../services/note/create'; import post from '../../../../services/note/create';
import { IRemoteUser, User } from '../../../../models/entities/user'; import { IRemoteUser, User } from '../../../../models/entities/user';
import { IAnnounce, IObject, getApId, getApIds } from '../../type'; import { IAnnounce, getApId, getApIds } from '../../type';
import { fetchNote, resolveNote } from '../../models/note'; import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person'; import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger'; import { apLogger } from '../../logger';
@ -14,7 +14,7 @@ const logger = apLogger;
/** /**
* アナウンスアクティビティを捌きます * アナウンスアクティビティを捌きます
*/ */
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: IObject): Promise<void> { export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
const uri = getApId(activity); const uri = getApId(activity);
// アナウンサーが凍結されていたらスキップ // アナウンサーが凍結されていたらスキップ
@ -38,14 +38,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
// Announce対象をresolve // Announce対象をresolve
let renote; let renote;
try { try {
renote = await resolveNote(note); renote = await resolveNote(targetUri);
} catch (e) { } catch (e) {
// 対象が4xxならスキップ // 対象が4xxならスキップ
if (e.statusCode >= 400 && e.statusCode < 500) { if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`); logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
return; return;
} }
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`); logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
throw e; throw e;
} }

View File

@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
const resolver = new Resolver(); const resolver = new Resolver();
let object; const object = await resolver.resolve(activity.object).catch(e => {
try {
object = await resolver.resolve(activity.object);
} catch (e) {
logger.error(`Resolution failed: ${e}`); logger.error(`Resolution failed: ${e}`);
throw e; throw e;
} });
if (validPost.includes(object.type)) { if (validPost.includes(object.type)) {
createNote(resolver, actor, object); createNote(resolver, actor, object);

View File

@ -1,8 +1,9 @@
import { IObject, isCreate, isDelete, isUpdate, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection } from '../type'; import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection } from '../type';
import { IRemoteUser } from '../../../models/entities/user'; import { IRemoteUser } from '../../../models/entities/user';
import create from './create'; import create from './create';
import performDeleteActivity from './delete'; import performDeleteActivity from './delete';
import performUpdateActivity from './update'; import performUpdateActivity from './update';
import { performReadActivity } from './read';
import follow from './follow'; import follow from './follow';
import undo from './undo'; import undo from './undo';
import like from './like'; import like from './like';
@ -41,6 +42,8 @@ async function performOneActivity(actor: IRemoteUser, activity: IObject): Promis
await performDeleteActivity(actor, activity); await performDeleteActivity(actor, activity);
} else if (isUpdate(activity)) { } else if (isUpdate(activity)) {
await performUpdateActivity(actor, activity); await performUpdateActivity(actor, activity);
} else if (isRead(activity)) {
await performReadActivity(actor, activity);
} else if (isFollow(activity)) { } else if (isFollow(activity)) {
await follow(actor, activity); await follow(actor, activity);
} else if (isAccept(activity)) { } else if (isAccept(activity)) {

View File

@ -0,0 +1,27 @@
import { IRemoteUser } from '../../../models/entities/user';
import { IRead, getApId } from '../type';
import { isSelfHost, extractDbHost } from '../../../misc/convert-host';
import { MessagingMessages } from '../../../models';
import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message';
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
const id = await getApId(activity.object);
if (!isSelfHost(extractDbHost(id))) {
return `skip: Read to foreign host (${id})`;
}
const messageId = id.split('/').pop();
const message = await MessagingMessages.findOne(messageId);
if (message == null) {
return `skip: message not found`;
}
if (actor.id != message.recipientId) {
return `skip: actor is not a message recipient`;
}
await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
};

View File

@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
const resolver = new Resolver(); const resolver = new Resolver();
let object; const object = await resolver.resolve(activity.object).catch(e => {
try {
object = await resolver.resolve(activity.object);
} catch (e) {
logger.error(`Resolution failed: ${e}`); logger.error(`Resolution failed: ${e}`);
throw e; throw e;
} });
switch (object.type) { switch (object.type) {
case 'Follow': case 'Follow':

View File

@ -20,14 +20,10 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
const resolver = new Resolver(); const resolver = new Resolver();
let object; const object = await resolver.resolve(activity.object).catch(e => {
try {
object = await resolver.resolve(activity.object);
} catch (e) {
logger.error(`Resolution failed: ${e}`); logger.error(`Resolution failed: ${e}`);
throw e; throw e;
} });
switch (object.type) { switch (object.type) {
case 'Follow': case 'Follow':

View File

@ -162,16 +162,42 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
// 引用 // 引用
let quote: Note | undefined | null; let quote: Note | undefined | null;
if (note._misskey_quote && typeof note._misskey_quote == 'string') { if (note._misskey_quote || note.quoteUrl) {
quote = await resolveNote(note._misskey_quote).catch(e => { const tryResolveNote = async (uri: string): Promise<{
// 4xxの場合は引用してないことにする status: 'ok';
if (e.statusCode >= 400 && e.statusCode < 500) { res: Note | null;
logger.warn(`Ignored quote target ${note.inReplyTo} - ${e.statusCode} `); } | {
return null; status: 'permerror' | 'temperror';
}> => {
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
try {
const res = await resolveNote(uri);
if (res) {
return {
status: 'ok',
res
};
} else {
return {
status: 'permerror'
};
}
} catch (e) {
return {
status: e.statusCode >= 400 && e.statusCode < 500 ? 'permerror' : 'temperror'
};
}
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
if (!quote) {
if (results.some(x => x.status === 'temperror')) {
throw 'quote resolve failed';
}
} }
logger.warn(`Error in quote target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
});
} }
const cw = note.summary === '' ? null : note.summary; const cw = note.summary === '' ? null : note.summary;
@ -226,7 +252,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
if (note._misskey_talk && visibility === 'specified') { if (note._misskey_talk && visibility === 'specified') {
for (const recipient of visibleUsers) { for (const recipient of visibleUsers) {
await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null); await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
return null; return null;
} }
} }

View File

@ -4,7 +4,6 @@ import config from '../../../config';
import Resolver from '../resolver'; import Resolver from '../resolver';
import { resolveImage } from './image'; import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type'; import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type';
import { DriveFile } from '../../../models/entities/drive-file';
import { fromHtml } from '../../../mfm/fromHtml'; import { fromHtml } from '../../../mfm/fromHtml';
import { resolveNote, extractEmojis } from './note'; import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
@ -201,18 +200,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
updateUsertags(user!, tags); updateUsertags(user!, tags);
//#region アバターとヘッダー画像をフェッチ //#region アバターとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<DriveFile | null>([ const [avatar, banner] = await Promise.all([
person.icon, person.icon,
person.image person.image
].map(img => ].map(img =>
img == null img == null
? Promise.resolve(null) ? Promise.resolve(null)
: resolveImage(user!, img).catch(() => null) : resolveImage(user!, img).catch(() => null)
))); ));
const avatarId = avatar ? avatar.id : null; const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null; const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null; const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null; const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null; const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
@ -290,14 +289,14 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
logger.info(`Updating the Person: ${person.id}`); logger.info(`Updating the Person: ${person.id}`);
// アバターとヘッダー画像をフェッチ // アバターとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<DriveFile | null>([ const [avatar, banner] = await Promise.all([
person.icon, person.icon,
person.image person.image
].map(img => ].map(img =>
img == null img == null
? Promise.resolve(null) ? Promise.resolve(null)
: resolveImage(exist, img).catch(() => null) : resolveImage(exist, img).catch(() => null)
))); ));
// カスタム絵文字取得 // カスタム絵文字取得
const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => { const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
@ -326,7 +325,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
if (avatar) { if (avatar) {
updates.avatarId = avatar.id; updates.avatarId = avatar.id;
updates.avatarUrl = DriveFiles.getPublicUrl(avatar); updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null; updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
} }

View File

@ -159,6 +159,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
content, content,
_misskey_content: text, _misskey_content: text,
_misskey_quote: quote, _misskey_quote: quote,
quoteUrl: quote,
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
to, to,
cc, cc,

View File

@ -6,7 +6,7 @@
* @param last URL of last page (optional) * @param last URL of last page (optional)
* @param orderedItems attached objects (optional) * @param orderedItems attached objects (optional)
*/ */
export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) { export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: object) {
const page: any = { const page: any = {
id, id,
type: 'OrderedCollection', type: 'OrderedCollection',

View File

@ -0,0 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/entities/user';
import { MessagingMessage } from '../../../models/entities/messaging-message';
export const renderReadActivity = (user: ILocalUser, message: MessagingMessage) => ({
type: 'Read',
actor: `${config.url}/users/${user.id}`,
object: message.uri
});

View File

@ -6,15 +6,10 @@ import * as cache from 'lookup-dns-cache';
import config from '../../config'; import config from '../../config';
import { ILocalUser } from '../../models/entities/user'; import { ILocalUser } from '../../models/entities/user';
import { publishApLogStream } from '../../services/stream'; import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger'; import { UserKeypairs } from '../../models';
import { UserKeypairs, Instances } from '../../models';
import { fetchMeta } from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import * as httpsProxyAgent from 'https-proxy-agent'; import * as httpsProxyAgent from 'https-proxy-agent';
export const logger = apLogger.createSubLogger('deliver');
const agent = config.proxy const agent = config.proxy
? new httpsProxyAgent(config.proxy) ? new httpsProxyAgent(config.proxy)
: new https.Agent({ : new https.Agent({
@ -24,28 +19,7 @@ const agent = config.proxy
export default async (user: ILocalUser, url: string, object: any) => { export default async (user: ILocalUser, url: string, object: any) => {
const timeout = 10 * 1000; const timeout = 10 * 1000;
const { protocol, host, hostname, port, pathname, search } = new URL(url); const { protocol, hostname, port, pathname, search } = new URL(url);
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(toPuny(host))) {
logger.info(`skip (blocked) ${url}`);
return;
}
// closedなら中断
const closedHosts = await Instances.find({
where: {
isMarkedAsClosed: true
},
cache: 60 * 1000
});
if (closedHosts.map(x => x.host).includes(toPuny(host))) {
logger.info(`skip (closed) ${url}`);
return;
}
logger.info(`--> ${url}`);
const data = JSON.stringify(object); const data = JSON.stringify(object);
@ -73,10 +47,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
} }
}, res => { }, res => {
if (res.statusCode! >= 400) { if (res.statusCode! >= 400) {
logger.warn(`${url} --> ${res.statusCode}`);
reject(res); reject(res);
} else { } else {
logger.succ(`${url} --> ${res.statusCode}`);
resolve(); resolve();
} }
}); });
@ -88,11 +60,6 @@ export default async (user: ILocalUser, url: string, object: any) => {
headers: ['date', 'host', 'digest'] headers: ['date', 'host', 'digest']
}); });
// Signature: Signature ... => Signature: ...
let sig = req.getHeader('Signature')!.toString();
sig = sig.replace(/^Signature /, '');
req.setHeader('Signature', sig);
req.on('timeout', () => req.abort()); req.on('timeout', () => req.abort());
req.on('error', e => { req.on('error', e => {

View File

@ -51,6 +51,13 @@ export default class Resolver {
Accept: 'application/activity+json, application/ld+json' Accept: 'application/activity+json, application/ld+json'
}, },
json: true json: true
}).catch(e => {
const message = `${e.name}: ${e.message ? e.message.substr(0, 200) : undefined}, url=${value}`;
throw {
name: e.name,
statusCode: e.statusCode,
message,
};
}); });
if (object == null || ( if (object == null || (

View File

@ -75,6 +75,7 @@ export interface INote extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
_misskey_content?: string; _misskey_content?: string;
_misskey_quote?: string; _misskey_quote?: string;
quoteUrl?: string;
_misskey_talk: boolean; _misskey_talk: boolean;
} }
@ -82,6 +83,7 @@ export interface IQuestion extends IObject {
type: 'Note' | 'Question'; type: 'Note' | 'Question';
_misskey_content?: string; _misskey_content?: string;
_misskey_quote?: string; _misskey_quote?: string;
quoteUrl?: string;
oneOf?: IQuestionChoice[]; oneOf?: IQuestionChoice[];
anyOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[];
endTime?: Date; endTime?: Date;
@ -140,6 +142,10 @@ export interface IUpdate extends IActivity {
type: 'Update'; type: 'Update';
} }
export interface IRead extends IActivity {
type: 'Read';
}
export interface IUndo extends IActivity { export interface IUndo extends IActivity {
type: 'Undo'; type: 'Undo';
} }
@ -180,6 +186,7 @@ export interface IBlock extends IActivity {
export const isCreate = (object: IObject): object is ICreate => object.type === 'Create'; export const isCreate = (object: IObject): object is ICreate => object.type === 'Create';
export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete'; export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete';
export const isUpdate = (object: IObject): object is IUpdate => object.type === 'Update'; export const isUpdate = (object: IObject): object is IUpdate => object.type === 'Update';
export const isRead = (object: IObject): object is IRead => object.type === 'Read';
export const isUndo = (object: IObject): object is IUndo => object.type === 'Undo'; export const isUndo = (object: IObject): object is IUndo => object.type === 'Undo';
export const isFollow = (object: IObject): object is IFollow => object.type === 'Follow'; export const isFollow = (object: IObject): object is IFollow => object.type === 'Follow';
export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept'; export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept';

View File

@ -2,7 +2,7 @@ import webFinger from './webfinger';
import config from '../config'; import config from '../config';
import { createPerson, updatePerson } from './activitypub/models/person'; import { createPerson, updatePerson } from './activitypub/models/person';
import { remoteLogger } from './logger'; import { remoteLogger } from './logger';
import chalk from 'chalk'; import * as chalk from 'chalk';
import { User, IRemoteUser } from '../models/entities/user'; import { User, IRemoteUser } from '../models/entities/user';
import { Users } from '../models'; import { Users } from '../models';
import { toPuny } from '../misc/convert-host'; import { toPuny } from '../misc/convert-host';

View File

@ -26,8 +26,6 @@ const router = new Router();
function inbox(ctx: Router.RouterContext) { function inbox(ctx: Router.RouterContext) {
let signature; let signature;
ctx.req.headers.authorization = `Signature ${ctx.req.headers.signature}`;
try { try {
signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
} catch (e) { } catch (e) {
@ -167,7 +165,8 @@ router.get('/users/:user', async (ctx, next) => {
const user = await Users.findOne({ const user = await Users.findOne({
id: userId, id: userId,
host: null host: null,
isSuspended: false
}); });
await userInfo(ctx, user); await userInfo(ctx, user);
@ -178,7 +177,8 @@ router.get('/@:user', async (ctx, next) => {
const user = await Users.findOne({ const user = await Users.findOne({
usernameLower: ctx.params.user.toLowerCase(), usernameLower: ctx.params.user.toLowerCase(),
host: null host: null,
isSuspended: false
}); });
await userInfo(ctx, user); await userInfo(ctx, user);

View File

@ -5,7 +5,7 @@ import authenticate from './authenticate';
import call from './call'; import call from './call';
import { ApiError } from './error'; import { ApiError } from './error';
export default (endpoint: IEndpoint, ctx: Koa.BaseContext) => new Promise((res) => { export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
const body = ctx.request.body; const body = ctx.request.body;
const reply = (x?: any, y?: ApiError) => { const reply = (x?: any, y?: ApiError) => {

View File

@ -1,12 +1,17 @@
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream';
import { publishMessagingIndexStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream';
import { User } from '../../../models/entities/user'; import { User, ILocalUser, IRemoteUser } from '../../../models/entities/user';
import { MessagingMessage } from '../../../models/entities/messaging-message'; import { MessagingMessage } from '../../../models/entities/messaging-message';
import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; import { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { IdentifiableError } from '../../../misc/identifiable-error'; import { IdentifiableError } from '../../../misc/identifiable-error';
import { UserGroup } from '../../../models/entities/user-group'; import { UserGroup } from '../../../models/entities/user-group';
import { toArray } from '../../../prelude/array';
import { renderReadActivity } from '../../../remote/activitypub/renderer/read';
import { renderActivity } from '../../../remote/activitypub/renderer';
import { deliver } from '../../../queue';
import orderedCollection from '../../../remote/activitypub/renderer/ordered-collection';
/** /**
* Mark messages as read * Mark messages as read
@ -101,3 +106,17 @@ export async function readGroupMessagingMessage(
publishMainStream(userId, 'readAllMessagingMessages'); publishMainStream(userId, 'readAllMessagingMessages');
} }
} }
export async function deliverReadActivity(user: ILocalUser, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
messages = toArray(messages).filter(x => x.uri);
const contents = messages.map(x => renderReadActivity(user, x));
if (contents.length > 1) {
const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
deliver(user, renderActivity(collection), recipient.inbox);
} else {
for (const content of contents) {
deliver(user, renderActivity(content), recipient.inbox);
}
}
}

View File

@ -6,7 +6,7 @@ import { Signins } from '../../../models';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { publishMainStream } from '../../../services/stream'; import { publishMainStream } from '../../../services/stream';
export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) { export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
if (redirect) { if (redirect) {
//#region Cookie //#region Cookie
const expires = 1000 * 60 * 60 * 24 * 365; // One Year const expires = 1000 * 60 * 60 * 24 * 365; // One Year

View File

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../../define'; import define from '../../../define';
import { deliverQueue, inboxQueue } from '../../../../../queue'; import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '../../../../../queue';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -10,11 +10,11 @@ export const meta = {
params: { params: {
domain: { domain: {
validator: $.str, validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']),
}, },
state: { state: {
validator: $.str, validator: $.str.or(['active', 'waiting', 'delayed']),
}, },
limit: { limit: {
@ -28,13 +28,22 @@ export default define(meta, async (ps) => {
const queue = const queue =
ps.domain === 'deliver' ? deliverQueue : ps.domain === 'deliver' ? deliverQueue :
ps.domain === 'inbox' ? inboxQueue : ps.domain === 'inbox' ? inboxQueue :
ps.domain === 'db' ? dbQueue :
ps.domain === 'objectStorage' ? objectStorageQueue :
null as never; null as never;
const jobs = await queue.getJobs([ps.state], 0, ps.limit!); const jobs = await queue.getJobs([ps.state], 0, ps.limit!);
return jobs.map(job => ({ return jobs.map(job => {
const data = job.data;
delete data.content;
delete data.user;
return {
id: job.id, id: job.id,
data: job.data, data,
attempts: job.attemptsMade, attempts: job.attemptsMade,
})); maxAttempts: job.opts ? job.opts.attempts : 0,
timestamp: job.timestamp,
};
});
}); });

View File

@ -2,7 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import deleteFollowing from '../../../../services/following/delete'; import deleteFollowing from '../../../../services/following/delete';
import { Users, Followings } from '../../../../models'; import { Users, Followings, Notifications } from '../../../../models';
import { User } from '../../../../models/entities/user'; import { User } from '../../../../models/entities/user';
import { insertModerationLog } from '../../../../services/insert-moderation-log'; import { insertModerationLog } from '../../../../services/insert-moderation-log';
import { doPostSuspend } from '../../../../services/suspend-user'; import { doPostSuspend } from '../../../../services/suspend-user';
@ -55,6 +55,7 @@ export default define(meta, async (ps, me) => {
(async () => { (async () => {
await doPostSuspend(user).catch(e => {}); await doPostSuspend(user).catch(e => {});
await unFollowAll(user).catch(e => {}); await unFollowAll(user).catch(e => {});
await readAllNotify(user).catch(e => {});
})(); })();
}); });
@ -75,3 +76,12 @@ async function unFollowAll(follower: User) {
await deleteFollowing(follower, followee, true); await deleteFollowing(follower, followee, true);
} }
} }
async function readAllNotify(notifier: User) {
await Notifications.update({
notifierId: notifier.id,
isRead: false,
}, {
isRead: true
});
}

View File

@ -151,6 +151,13 @@ export const meta = {
} }
}, },
proxyRemoteFiles: {
validator: $.optional.bool,
desc: {
'ja-JP': 'ローカルにないリモートのファイルをプロキシするか否か'
}
},
enableRecaptcha: { enableRecaptcha: {
validator: $.optional.bool, validator: $.optional.bool,
desc: { desc: {
@ -478,6 +485,10 @@ export default define(meta, async (ps, me) => {
set.cacheRemoteFiles = ps.cacheRemoteFiles; set.cacheRemoteFiles = ps.cacheRemoteFiles;
} }
if (ps.proxyRemoteFiles !== undefined) {
set.proxyRemoteFiles = ps.proxyRemoteFiles;
}
if (ps.enableRecaptcha !== undefined) { if (ps.enableRecaptcha !== undefined) {
set.enableRecaptcha = ps.enableRecaptcha; set.enableRecaptcha = ps.enableRecaptcha;
} }

View File

@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
import { readNotification } from '../../common/read-notification'; import { readNotification } from '../../common/read-notification';
import define from '../../define'; import define from '../../define';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notifications, Followings, Mutings } from '../../../../models'; import { Notifications, Followings, Mutings, Users } from '../../../../models';
export const meta = { export const meta = {
desc: { desc: {
@ -72,6 +72,10 @@ export default define(meta, async (ps, user) => {
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: user.id }); .where('muting.muterId = :muterId', { muterId: user.id });
const suspendedQuery = Users.createQueryBuilder('users')
.select('id')
.where('users.isSuspended = TRUE');
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
.andWhere(`notification.notifieeId = :meId`, { meId: user.id }) .andWhere(`notification.notifieeId = :meId`, { meId: user.id })
.leftJoinAndSelect('notification.notifier', 'notifier'); .leftJoinAndSelect('notification.notifier', 'notifier');
@ -79,6 +83,8 @@ export default define(meta, async (ps, user) => {
query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`); query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
query.setParameters(mutingQuery.getParameters()); query.setParameters(mutingQuery.getParameters());
query.andWhere(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`);
if (ps.following) { if (ps.following) {
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
query.setParameters(followingQuery.getParameters()); query.setParameters(followingQuery.getParameters());

View File

@ -3,10 +3,10 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { getUser } from '../../common/getters'; import { getUser } from '../../common/getters';
import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models'; import { MessagingMessages, UserGroups, UserGroupJoinings, Users } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
export const meta = { export const meta = {
desc: { desc: {
@ -114,6 +114,11 @@ export default define(meta, async (ps, user) => {
// Mark all as read // Mark all as read
if (ps.markAsRead) { if (ps.markAsRead) {
readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id)); readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id));
// リモートユーザーとのメッセージだったら既読配信
if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
deliverReadActivity(user, recipient, messages);
}
} }
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {

View File

@ -143,6 +143,7 @@ export default define(meta, async (ps, me) => {
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
proxyRemoteFiles: instance.proxyRemoteFiles,
enableRecaptcha: instance.enableRecaptcha, enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey, recaptchaSiteKey: instance.recaptchaSiteKey,
swPublickey: instance.swPublicKey, swPublickey: instance.swPublicKey,

View File

@ -66,7 +66,7 @@ export default define(meta, async (ps, user) => {
})) }))
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (user) generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMuteQuery(query, user);
const notes = await query.take(ps.limit!).getMany(); const notes = await query.take(ps.limit!).getMany();

View File

@ -95,7 +95,7 @@ export default define(meta, async (ps, user) => {
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (user) generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMuteQuery(query, user);
if (ps.withFiles) { if (ps.withFiles) {

View File

@ -70,7 +70,7 @@ export default define(meta, async (ps, user) => {
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (user) generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMuteQuery(query, user);
const renotes = await query.take(ps.limit!).getMany(); const renotes = await query.take(ps.limit!).getMany();

View File

@ -61,7 +61,7 @@ export default define(meta, async (ps, user) => {
.andWhere('note.replyId = :replyId', { replyId: ps.noteId }) .andWhere('note.replyId = :replyId', { replyId: ps.noteId })
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (user) generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMuteQuery(query, user);
const timeline = await query.take(ps.limit!).getMany(); const timeline = await query.take(ps.limit!).getMany();

View File

@ -95,7 +95,7 @@ export default define(meta, async (ps, me) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (me) generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me); if (me) generateMuteQuery(query, me);
if (ps.tag) { if (ps.tag) {

View File

@ -133,7 +133,7 @@ export default define(meta, async (ps, me) => {
.andWhere('note.userId = :userId', { userId: user.id }) .andWhere('note.userId = :userId', { userId: user.id })
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (me) generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me, user); if (me) generateMuteQuery(query, me, user);
if (ps.withFiles) { if (ps.withFiles) {

View File

@ -66,13 +66,18 @@ export const meta = {
export default define(meta, async (ps, me) => { export default define(meta, async (ps, me) => {
let user; let user;
const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
if (ps.userIds) { if (ps.userIds) {
if (ps.userIds.length === 0) { if (ps.userIds.length === 0) {
return []; return [];
} }
const users = await Users.find({ const users = await Users.find(isAdminOrModerator ? {
id: In(ps.userIds) id: In(ps.userIds)
} : {
id: In(ps.userIds),
isSuspended: false
}); });
return await Promise.all(users.map(u => Users.pack(u, me, { return await Promise.all(users.map(u => Users.pack(u, me, {
@ -93,7 +98,7 @@ export default define(meta, async (ps, me) => {
user = await Users.findOne(q); user = await Users.findOne(q);
} }
if (user == null) { if (user == null || (!isAdminOrModerator && user.isSuspended)) {
throw new ApiError(meta.errors.noSuchUser); throw new ApiError(meta.errors.noSuchUser);
} }

View File

@ -10,7 +10,7 @@ import { ensure } from '../../../prelude/ensure';
import { verifyLogin, hash } from '../2fa'; import { verifyLogin, hash } from '../2fa';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
export default async (ctx: Koa.BaseContext) => { export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Origin', config.url);
ctx.set('Access-Control-Allow-Credentials', 'true'); ctx.set('Access-Control-Allow-Credentials', 'true');

View File

@ -15,8 +15,8 @@ import { UserProfile } from '../../../models/entities/user-profile';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { UsedUsername } from '../../../models/entities/used-username'; import { UsedUsername } from '../../../models/entities/used-username';
export default async (ctx: Koa.BaseContext) => { export default async (ctx: Koa.Context) => {
const body = ctx.request.body as any; const body = ctx.request.body;
const instance = await fetchMeta(true); const instance = await fetchMeta(true);

View File

@ -12,11 +12,11 @@ import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.BaseContext) { function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) { function normalizeUrl(url: string) {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
} }

View File

@ -12,11 +12,11 @@ import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.BaseContext) { function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) { function normalizeUrl(url: string) {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
} }

View File

@ -11,11 +11,11 @@ import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.Context) {
return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1]; return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
} }
function compareOrigin(ctx: Koa.BaseContext) { function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) { function normalizeUrl(url: string) {
return url.endsWith('/') ? url.substr(0, url.length - 1) : url; return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
} }

View File

@ -25,17 +25,8 @@ export default class extends Channel {
@autobind @autobind
private async onNote(note: PackedNote) { private async onNote(note: PackedNote) {
if ((note.user as PackedUser).host !== null) return; if ((note.user as PackedUser).host !== null) return;
if (note.visibility === 'home') return; if (note.visibility !== 'public') return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack // リプライなら再pack
if (note.replyId != null) { if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, { note.reply = await Notes.pack(note.replyId, this.user, {
@ -48,7 +39,6 @@ export default class extends Channel {
detail: true detail: true
}); });
} }
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return; if (shouldMuteThisNote(note, this.muting)) return;

View File

@ -1,7 +1,8 @@
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
import Channel from '../channel'; import Channel from '../channel';
import { UserGroupJoinings } from '../../../../models'; import { UserGroupJoinings, Users, MessagingMessages } from '../../../../models';
import { User, ILocalUser, IRemoteUser } from '../../../../models/entities/user';
export default class extends Channel { export default class extends Channel {
public readonly chName = 'messaging'; public readonly chName = 'messaging';
@ -9,11 +10,13 @@ export default class extends Channel {
public static requireCredential = true; public static requireCredential = true;
private otherpartyId: string | null; private otherpartyId: string | null;
private otherparty?: User;
private groupId: string | null; private groupId: string | null;
@autobind @autobind
public async init(params: any) { public async init(params: any) {
this.otherpartyId = params.otherparty as string; this.otherpartyId = params.otherparty as string;
this.otherparty = await Users.findOne({ id: this.otherpartyId });
this.groupId = params.group as string; this.groupId = params.group as string;
// Check joining // Check joining
@ -44,6 +47,13 @@ export default class extends Channel {
case 'read': case 'read':
if (this.otherpartyId) { if (this.otherpartyId) {
readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
// リモートユーザーからのメッセージだったら既読配信
if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) {
MessagingMessages.findOne(body.id).then(message => {
if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message);
});
}
} else if (this.groupId) { } else if (this.groupId) {
readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
} }

View File

@ -26,7 +26,7 @@ module.exports = (server: http.Server) => {
const subscriber = redis.createClient( const subscriber = redis.createClient(
config.redis.port, config.redis.host); config.redis.port, config.redis.host);
subscriber.subscribe('misskey'); subscriber.subscribe(config.host);
ev = new EventEmitter(); ev = new EventEmitter();

View File

@ -12,19 +12,14 @@ import sendDriveFile from './send-drive-file';
const app = new Koa(); const app = new Koa();
app.use(cors()); app.use(cors());
app.use(async (ctx, next) => {
// Cache 365days
ctx.set('Cache-Control', 'max-age=31536000, immutable');
await next();
});
// Init router // Init router
const router = new Router(); const router = new Router();
router.get('/app-default.jpg', ctx => { router.get('/app-default.jpg', ctx => {
const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); const file = fs.createReadStream(`${__dirname}/assets/dummy.png`);
ctx.set('Content-Type', 'image/jpeg');
ctx.body = file; ctx.body = file;
ctx.set('Content-Type', 'image/jpeg');
ctx.set('Cache-Control', 'max-age=31536000, immutable');
}); });
router.get('/:key', sendDriveFile); router.get('/:key', sendDriveFile);

View File

@ -1,19 +1,26 @@
import * as Koa from 'koa'; import * as Koa from 'koa';
import * as send from 'koa-send'; import * as send from 'koa-send';
import * as rename from 'rename'; import * as rename from 'rename';
import * as tmp from 'tmp';
import * as fs from 'fs';
import { serverLogger } from '..'; import { serverLogger } from '..';
import { contentDisposition } from '../../misc/content-disposition'; import { contentDisposition } from '../../misc/content-disposition';
import { DriveFiles } from '../../models'; import { DriveFiles } from '../../models';
import { InternalStorage } from '../../services/drive/internal-storage'; import { InternalStorage } from '../../services/drive/internal-storage';
import { downloadUrl } from '../../misc/donwload-url';
import { detectMine } from '../../misc/detect-mine';
import { convertToJpeg, convertToPng } from '../../services/drive/image-processor';
import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail';
const assets = `${__dirname}/../../server/file/assets/`; const assets = `${__dirname}/../../server/file/assets/`;
const commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): void => { const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
serverLogger.error(e); serverLogger.error(e);
ctx.status = 500; ctx.status = 500;
ctx.set('Cache-Control', 'max-age=300');
}; };
export default async function(ctx: Koa.BaseContext) { export default async function(ctx: Koa.Context) {
const key = ctx.params.key; const key = ctx.params.key;
// Fetch drive file // Fetch drive file
@ -25,32 +32,88 @@ export default async function(ctx: Koa.BaseContext) {
if (file == null) { if (file == null) {
ctx.status = 404; ctx.status = 404;
ctx.set('Cache-Control', 'max-age=86400');
await send(ctx as any, '/dummy.png', { root: assets }); await send(ctx as any, '/dummy.png', { root: assets });
return; return;
} }
if (!file.storedInternal) {
ctx.status = 204;
return;
}
const isThumbnail = file.thumbnailAccessKey === key; const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key; const isWebpublic = file.webpublicAccessKey === key;
if (isThumbnail) { if (!file.storedInternal) {
ctx.set('Content-Type', 'image/jpeg'); if (file.isLink && file.uri) { // 期限切れリモートファイル
ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`)); const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
ctx.body = InternalStorage.read(key); tmp.file((e, path, fd, cleanup) => {
} else if (isWebpublic) { if (e) return rej(e);
ctx.set('Content-Type', file.type === 'image/apng' ? 'image/png' : file.type); res([path, cleanup]);
ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`)); });
ctx.body = InternalStorage.read(key); });
} else {
ctx.set('Content-Disposition', contentDisposition('inline', `${file.name}`));
try {
await downloadUrl(file.uri, path);
const [type, ext] = await detectMine(path);
const convertFile = async () => {
if (isThumbnail) {
if (['image/jpeg', 'image/webp'].includes(type)) {
return await convertToJpeg(path, 498, 280);
} else if (['image/png'].includes(type)) {
return await convertToPng(path, 498, 280);
} else if (type.startsWith('video/')) {
return await GenerateVideoThumbnail(path);
}
}
return {
data: fs.readFileSync(path),
ext,
type,
};
};
const image = await convertFile();
ctx.body = image.data;
ctx.set('Content-Type', image.type);
ctx.set('Cache-Control', 'max-age=31536000, immutable');
} catch (e) {
serverLogger.error(e);
if (typeof e == 'number' && e >= 400 && e < 500) {
ctx.status = e;
ctx.set('Cache-Control', 'max-age=86400');
} else {
ctx.status = 500;
ctx.set('Cache-Control', 'max-age=300');
}
} finally {
cleanup();
}
return;
}
ctx.status = 204;
ctx.set('Cache-Control', 'max-age=86400');
return;
}
if (isThumbnail || isWebpublic) {
const [mime, ext] = await detectMine(InternalStorage.resolvePath(key));
const filename = rename(file.name, {
suffix: isThumbnail ? '-thumb' : '-web',
extname: ext ? `.${ext}` : undefined
}).toString();
ctx.body = InternalStorage.read(key);
ctx.set('Content-Type', mime);
ctx.set('Cache-Control', 'max-age=31536000, immutable');
ctx.set('Content-Disposition', contentDisposition('inline', filename));
} else {
const readable = InternalStorage.read(file.accessKey!); const readable = InternalStorage.read(file.accessKey!);
readable.on('error', commonReadableHandlerGenerator(ctx)); readable.on('error', commonReadableHandlerGenerator(ctx));
ctx.set('Content-Type', file.type);
ctx.body = readable; ctx.body = readable;
ctx.set('Content-Type', file.type);
ctx.set('Cache-Control', 'max-age=31536000, immutable');
ctx.set('Content-Disposition', contentDisposition('inline', file.name));
} }
} }

View File

@ -6,7 +6,7 @@ import { createTemp } from '../../misc/create-temp';
import { downloadUrl } from '../../misc/donwload-url'; import { downloadUrl } from '../../misc/donwload-url';
import { detectMine } from '../../misc/detect-mine'; import { detectMine } from '../../misc/detect-mine';
export async function proxyMedia(ctx: Koa.BaseContext) { export async function proxyMedia(ctx: Koa.Context) {
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
// Create temp file // Create temp file

View File

@ -101,7 +101,8 @@ const getFeed = async (acct: string) => {
const { username, host } = parseAcct(acct); const { username, host } = parseAcct(acct);
const user = await Users.findOne({ const user = await Users.findOne({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host host,
isSuspended: false
}); });
return user && await packFeed(user); return user && await packFeed(user);
@ -149,7 +150,8 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
const { username, host } = parseAcct(ctx.params.user); const { username, host } = parseAcct(ctx.params.user);
const user = await Users.findOne({ const user = await Users.findOne({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host host,
isSuspended: false
}); });
if (user != null) { if (user != null) {
@ -170,6 +172,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
ctx.set('Cache-Control', 'public, max-age=30'); ctx.set('Cache-Control', 'public, max-age=30');
} else { } else {
// リモートユーザーなので // リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
await next(); await next();
} }
}); });
@ -177,7 +180,8 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
router.get('/users/:user', async ctx => { router.get('/users/:user', async ctx => {
const user = await Users.findOne({ const user = await Users.findOne({
id: ctx.params.user, id: ctx.params.user,
host: null host: null,
isSuspended: false
}); });
if (user == null) { if (user == null) {

View File

@ -2,7 +2,7 @@ import * as Koa from 'koa';
import * as manifest from '../../client/assets/manifest.json'; import * as manifest from '../../client/assets/manifest.json';
import { fetchMeta } from '../../misc/fetch-meta'; import { fetchMeta } from '../../misc/fetch-meta';
module.exports = async (ctx: Koa.BaseContext) => { module.exports = async (ctx: Koa.Context) => {
const json = JSON.parse(JSON.stringify(manifest)); const json = JSON.parse(JSON.stringify(manifest));
const instance = await fetchMeta(true); const instance = await fetchMeta(true);

View File

@ -8,7 +8,7 @@ import { query } from '../../prelude/url';
const logger = new Logger('url-preview'); const logger = new Logger('url-preview');
module.exports = async (ctx: Koa.BaseContext) => { module.exports = async (ctx: Koa.Context) => {
const meta = await fetchMeta(); const meta = await fetchMeta();
logger.info(meta.summalyProxy logger.info(meta.summalyProxy

View File

@ -49,7 +49,8 @@ router.get('/.well-known/nodeinfo', async ctx => {
router.get(webFingerPath, async ctx => { router.get(webFingerPath, async ctx => {
const fromId = (id: User['id']): Record<string, any> => ({ const fromId = (id: User['id']): Record<string, any> => ({
id, id,
host: null host: null,
isSuspended: false
}); });
const generateQuery = (resource: string) => const generateQuery = (resource: string) =>
@ -63,7 +64,8 @@ router.get(webFingerPath, async ctx => {
const fromAcct = (acct: Acct): Record<string, any> | number => const fromAcct = (acct: Acct): Record<string, any> | number =>
!acct.host || acct.host === config.host.toLowerCase() ? { !acct.host || acct.host === config.host.toLowerCase() ? {
usernameLower: acct.username, usernameLower: acct.username,
host: null host: null,
isSuspended: false
} : 422; } : 422;
if (typeof ctx.query.resource !== 'string') { if (typeof ctx.query.resource !== 'string') {

View File

@ -10,7 +10,7 @@ import { deleteFile } from './delete-file';
import { fetchMeta } from '../../misc/fetch-meta'; import { fetchMeta } from '../../misc/fetch-meta';
import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { GenerateVideoThumbnail } from './generate-video-thumbnail';
import { driveLogger } from './logger'; import { driveLogger } from './logger';
import { IImage, convertToJpeg, convertToWebp, convertToPng, convertToGif, convertToApng } from './image-processor'; import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor';
import { contentDisposition } from '../../misc/content-disposition'; import { contentDisposition } from '../../misc/content-disposition';
import { detectMine } from '../../misc/detect-mine'; import { detectMine } from '../../misc/detect-mine';
import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models';
@ -159,12 +159,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
webpublic = await convertToWebp(path, 2048, 2048); webpublic = await convertToWebp(path, 2048, 2048);
} else if (['image/png'].includes(type)) { } else if (['image/png'].includes(type)) {
webpublic = await convertToPng(path, 2048, 2048); webpublic = await convertToPng(path, 2048, 2048);
} else if (['image/apng', 'image/vnd.mozilla.apng'].includes(type)) {
webpublic = await convertToApng(path);
} else if (['image/gif'].includes(type)) {
webpublic = await convertToGif(path);
} else { } else {
logger.info(`web image not created (not an image)`); logger.debug(`web image not created (not an required image)`);
} }
} catch (e) { } catch (e) {
logger.warn(`web image not created (an error occured)`, e); logger.warn(`web image not created (an error occured)`, e);
@ -182,16 +178,14 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
thumbnail = await convertToJpeg(path, 498, 280); thumbnail = await convertToJpeg(path, 498, 280);
} else if (['image/png'].includes(type)) { } else if (['image/png'].includes(type)) {
thumbnail = await convertToPng(path, 498, 280); thumbnail = await convertToPng(path, 498, 280);
} else if (['image/gif'].includes(type)) {
thumbnail = await convertToGif(path);
} else if (['image/apng', 'image/vnd.mozilla.apng'].includes(type)) {
thumbnail = await convertToApng(path);
} else if (type.startsWith('video/')) { } else if (type.startsWith('video/')) {
try { try {
thumbnail = await GenerateVideoThumbnail(path); thumbnail = await GenerateVideoThumbnail(path);
} catch (e) { } catch (e) {
logger.error(`GenerateVideoThumbnail failed: ${e}`); logger.warn(`GenerateVideoThumbnail failed: ${e}`);
} }
} else {
logger.debug(`thumbnail not created (not an required file)`);
} }
} catch (e) { } catch (e) {
logger.warn(`thumbnail not created (an error occured)`, e); logger.warn(`thumbnail not created (an error occured)`, e);
@ -361,7 +355,7 @@ export default async function(
let propPromises: Promise<void>[] = []; let propPromises: Promise<void>[] = [];
const isImage = ['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp'].includes(mime); const isImage = ['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime);
if (isImage) { if (isImage) {
const img = sharp(path); const img = sharp(path);
@ -384,8 +378,9 @@ export default async function(
logger.debug('calculating average color...'); logger.debug('calculating average color...');
try { try {
const info = await (img as any).stats(); const info = await img.stats();
if (info.isOpaque) {
const r = Math.round(info.channels[0].mean); const r = Math.round(info.channels[0].mean);
const g = Math.round(info.channels[1].mean); const g = Math.round(info.channels[1].mean);
const b = Math.round(info.channels[2].mean); const b = Math.round(info.channels[2].mean);
@ -393,6 +388,11 @@ export default async function(
logger.debug(`average color is calculated: ${r}, ${g}, ${b}`); logger.debug(`average color is calculated: ${r}, ${g}, ${b}`);
properties['avgColor'] = `rgb(${r},${g},${b})`; properties['avgColor'] = `rgb(${r},${g},${b})`;
} else {
logger.debug(`this image is not opaque so average color is 255, 255, 255`);
properties['avgColor'] = `rgb(255,255,255)`;
}
} catch (e) { } } catch (e) { }
}; };
@ -422,8 +422,10 @@ export default async function(
if (isLink) { if (isLink) {
file.url = url; file.url = url;
file.thumbnailUrl = url; // ローカルプロキシ用
file.webpublicUrl = url; file.accessKey = uuid();
file.thumbnailAccessKey = 'thumbnail-' + uuid();
file.webpublicAccessKey = 'webpublic-' + uuid();
} }
} }

View File

@ -5,6 +5,7 @@ import { driveChart, perUserDriveChart, instanceChart } from '../chart';
import { createDeleteObjectStorageFileJob } from '../../queue'; import { createDeleteObjectStorageFileJob } from '../../queue';
import { fetchMeta } from '../../misc/fetch-meta'; import { fetchMeta } from '../../misc/fetch-meta';
import { getS3 } from './s3'; import { getS3 } from './s3';
import { v4 as uuid } from 'uuid';
export async function deleteFile(file: DriveFile, isExpired = false) { export async function deleteFile(file: DriveFile, isExpired = false) {
if (file.storedInternal) { if (file.storedInternal) {
@ -68,9 +69,13 @@ function postProcess(file: DriveFile, isExpired = false) {
DriveFiles.update(file.id, { DriveFiles.update(file.id, {
isLink: true, isLink: true,
url: file.uri, url: file.uri,
thumbnailUrl: file.uri, thumbnailUrl: null,
webpublicUrl: file.uri, webpublicUrl: null,
size: 0, size: 0,
// ローカルプロキシ用
accessKey: uuid(),
thumbnailAccessKey: 'thumbnail-' + uuid(),
webpublicAccessKey: 'webpublic-' + uuid(),
}); });
} else { } else {
DriveFiles.delete(file.id); DriveFiles.delete(file.id);

View File

@ -1,5 +1,4 @@
import * as sharp from 'sharp'; import * as sharp from 'sharp';
import * as fs from 'fs';
export type IImage = { export type IImage = {
data: Buffer; data: Buffer;
@ -74,29 +73,3 @@ export async function convertToPng(path: string, width: number, height: number):
type: 'image/png' type: 'image/png'
}; };
} }
/**
* Convert to GIF (Actually just NOP)
*/
export async function convertToGif(path: string): Promise<IImage> {
const data = await fs.promises.readFile(path);
return {
data,
ext: 'gif',
type: 'image/gif'
};
}
/**
* Convert to APNG (Actually just NOP)
*/
export async function convertToApng(path: string): Promise<IImage> {
const data = await fs.promises.readFile(path);
return {
data,
ext: 'apng',
type: 'image/apng'
};
}

View File

@ -3,25 +3,27 @@ import * as Path from 'path';
import config from '../../config'; import config from '../../config';
export class InternalStorage { export class InternalStorage {
private static readonly path = Path.resolve(`${__dirname}/../../../files`); private static readonly path = Path.resolve(__dirname, '../../../files');
public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);
public static read(key: string) { public static read(key: string) {
return fs.createReadStream(`${InternalStorage.path}/${key}`); return fs.createReadStream(InternalStorage.resolvePath(key));
} }
public static saveFromPath(key: string, srcPath: string) { public static saveFromPath(key: string, srcPath: string) {
fs.mkdirSync(InternalStorage.path, { recursive: true }); fs.mkdirSync(InternalStorage.path, { recursive: true });
fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`); fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
return `${config.url}/files/${key}`; return `${config.url}/files/${key}`;
} }
public static saveFromBuffer(key: string, data: Buffer) { public static saveFromBuffer(key: string, data: Buffer) {
fs.mkdirSync(InternalStorage.path, { recursive: true }); fs.mkdirSync(InternalStorage.path, { recursive: true });
fs.writeFileSync(`${InternalStorage.path}/${key}`, data); fs.writeFileSync(InternalStorage.resolvePath(key), data);
return `${config.url}/files/${key}`; return `${config.url}/files/${key}`;
} }
public static del(key: string) { public static del(key: string) {
fs.unlink(`${InternalStorage.path}/${key}`, () => {}); fs.unlink(InternalStorage.resolvePath(key), () => {});
} }
} }

View File

@ -24,6 +24,22 @@ export default async function(follower: User, followee: User, silent = false) {
await Followings.delete(following.id); await Followings.delete(following.id);
decrementFollowing(follower, followee);
// Publish unfollow event
if (!silent && Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
}
export async function decrementFollowing(follower: User, followee: User) {
//#region Decrement following count //#region Decrement following count
Users.decrement({ id: follower.id }, 'followingCount', 1); Users.decrement({ id: follower.id }, 'followingCount', 1);
//#endregion //#endregion
@ -47,16 +63,4 @@ export default async function(follower: User, followee: User, silent = false) {
//#endregion //#endregion
perUserFollowingChart.update(follower, followee, false); perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event
if (!silent && Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
}
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
} }

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