Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
16fb7c4557 | |||
8aafafe416 | |||
90cfd87f46 | |||
5ff89e1538 | |||
8b6968c665 | |||
6ef1b1b1a2 | |||
fc0e1955d7 | |||
c3b8123e32 | |||
b6ec3f655a | |||
e37840d870 | |||
85b7eb1fb8 | |||
541f5f1314 | |||
a3c7901f87 | |||
78ef0a9929 | |||
b0bb5d8dfc | |||
307fc18138 | |||
330f2dedf7 | |||
c5c074f201 | |||
b16e5bd136 | |||
e13f778b33 | |||
953142115c | |||
1eb5578063 | |||
9bc07c1a1c | |||
cbbdc98744 | |||
4229065a69 | |||
932436096f | |||
d95242cab0 | |||
4214a0618e | |||
c012f4f880 | |||
3e85aad80a | |||
648be3005f | |||
66165b1935 | |||
e9360ac892 | |||
1d234e10bd | |||
2a9de356db | |||
43f3f8a058 | |||
4998ba8866 | |||
d18291cf0c | |||
fe9371f06c | |||
05a15afadb | |||
93417912bb | |||
07a565a61a | |||
332b13dfd0 | |||
81477ea7ee | |||
39e84539cd | |||
6496fbf923 | |||
de57dd7c97 | |||
9985c010bc | |||
f7a328d66e | |||
50598bcefb | |||
9c38e9722a | |||
b241967fb9 | |||
1f86a6d329 | |||
7a6b5b0bfc | |||
e406791b7b | |||
4ce2f596ee | |||
567f71fe61 | |||
70bb5879f9 | |||
cfd2d84b14 | |||
44ab428803 | |||
b34b728fbb | |||
8ada1725bf | |||
873444c3c6 | |||
8bdd4fd061 | |||
f3b518fb62 |
@ -140,3 +140,6 @@ autoAdmin: true
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
53
CHANGELOG.md
53
CHANGELOG.md
@ -1,6 +1,59 @@
|
||||
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)
|
||||
--------------------
|
||||
### 🐛Fixes
|
||||
|
56
README.md
56
README.md
@ -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>
|
||||
|
||||
[](https://misskey.io/)
|
||||
[](https://join.misskey.page/)
|
||||
================================================================
|
||||
|
||||
[](https://circleci.com/gh/syuilo/misskey)
|
||||
@ -10,7 +10,7 @@
|
||||
**A forever evolving, sophisticated microblogging platform.**
|
||||
|
||||
<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),
|
||||
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>
|
||||
@ -103,76 +103,88 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
----------------------------------------------------------------
|
||||
<!-- PATREON_START -->
|
||||
<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/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/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/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>
|
||||
<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/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=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=557245">mkatze</a></td>
|
||||
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
|
||||
</tr></table>
|
||||
<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://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/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://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>
|
||||
<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/user?u=16869916">見当かなみ</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/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>
|
||||
<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/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/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/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>
|
||||
<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=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/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=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>
|
||||
<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/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://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>
|
||||
<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/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=12531784">Takashi Shibuya</a></td>
|
||||
</tr></table>
|
||||
|
||||
**Last updated:** Sat, 02 Nov 2019 18:09:05 UTC
|
||||
**Last updated:** Sun, 05 Jan 2020 05:37:07 UTC
|
||||
<!-- PATREON_END -->
|
||||
|
||||
: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).
|
||||
|
||||
|
23
gulpfile.ts
23
gulpfile.ts
@ -3,27 +3,22 @@
|
||||
*/
|
||||
|
||||
import * as gulp from 'gulp';
|
||||
import * as gutil from 'gulp-util';
|
||||
import * as ts from 'gulp-typescript';
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
import tslint from 'gulp-tslint';
|
||||
const cssnano = require('gulp-cssnano');
|
||||
const stylus = require('gulp-stylus');
|
||||
import * as uglifyComposer from 'gulp-uglify/composer';
|
||||
import * as rimraf from 'rimraf';
|
||||
import chalk from 'chalk';
|
||||
import * as chalk from 'chalk';
|
||||
import * as rename from 'gulp-rename';
|
||||
import * as mocha from 'gulp-mocha';
|
||||
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 uglify = uglifyComposer(uglifyes, console);
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const isProduction = env === 'production';
|
||||
const isDebug = !isProduction;
|
||||
const isDebug = env !== 'production';
|
||||
|
||||
if (isDebug) {
|
||||
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('ENV', JSON.stringify(env)))
|
||||
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
|
||||
.pipe(isProduction ? uglify({
|
||||
.pipe(terser({
|
||||
toplevel: true
|
||||
} as any) : gutil.noop())
|
||||
}))
|
||||
.pipe(gulp.dest('./built/client/assets/'));
|
||||
});
|
||||
|
||||
gulp.task('build:client:styles', () =>
|
||||
gulp.src('./src/client/app/init.css')
|
||||
.pipe(isProduction
|
||||
? (cssnano as any)()
|
||||
: gutil.noop())
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest('./built/client/assets/'))
|
||||
);
|
||||
|
||||
@ -130,7 +123,7 @@ gulp.task('copy:client', () =>
|
||||
gulp.task('doc', () =>
|
||||
gulp.src('./src/docs/**/*.styl')
|
||||
.pipe(stylus())
|
||||
.pipe((cssnano as any)())
|
||||
.pipe(cleanCSS())
|
||||
.pipe(gulp.dest('./built/docs/assets/'))
|
||||
);
|
||||
|
||||
|
@ -289,6 +289,7 @@ common:
|
||||
sync: "Synchronizace"
|
||||
save: "Uložit"
|
||||
saved: "Uloženo"
|
||||
preview: "Náhled"
|
||||
room: "Místnost"
|
||||
_room:
|
||||
graphicsQuality: "Kvalita grafiky"
|
||||
@ -399,6 +400,7 @@ common/views/components/games/reversi/reversi.index.vue:
|
||||
invite: "Pozvat"
|
||||
rule: "Jak hrát"
|
||||
mode-invite: "Pozvat"
|
||||
invitations: "Jste pozvaní ke hře!"
|
||||
my-games: "Moje hra"
|
||||
all-games: "Všechny hry"
|
||||
enter-username: "Zadejte své uživatelské jméno"
|
||||
|
@ -258,6 +258,7 @@ common:
|
||||
load-remote-media: "Vis medie-materiale fra en ekstern server"
|
||||
save: "Gem"
|
||||
saved: "Gemt"
|
||||
preview: "Før-visning"
|
||||
search: "Søg"
|
||||
delete: "Slet"
|
||||
loading: "Henter"
|
||||
@ -1018,6 +1019,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "Ordfilter"
|
||||
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)."
|
||||
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"
|
||||
common/views/components/password-settings.vue:
|
||||
reset: "Skift adgangskode"
|
||||
@ -1153,7 +1156,6 @@ admin/views/instance.vue:
|
||||
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\"."
|
||||
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"
|
||||
remote-drive-capacity-mb: "Kapacitet på hver ekstern brugers drev"
|
||||
mb: "I megabytes (MB)"
|
||||
@ -1794,6 +1796,8 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
_round:
|
||||
arg1: "Tal"
|
||||
eq: "A og B er ens"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
|
@ -251,6 +251,7 @@ common:
|
||||
load-remote-media: "Zeige Inhalte von fremden Servern"
|
||||
save: "Speichern"
|
||||
saved: "Gespeichert"
|
||||
preview: "Vorschau"
|
||||
search: "Suche"
|
||||
delete: "Löschen"
|
||||
loading: "Laden"
|
||||
@ -743,6 +744,7 @@ common/views/components/drive-settings.vue:
|
||||
in-use: "benutzt"
|
||||
stats: "Statistiken"
|
||||
common/views/components/mute-and-block.vue:
|
||||
unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?"
|
||||
save: "Speichern"
|
||||
desktop/views/components/sub-note-content.vue:
|
||||
private: "Dieser Beitrag ist privat"
|
||||
|
@ -300,6 +300,7 @@ common:
|
||||
sync: "Sync"
|
||||
save: "Save"
|
||||
saved: "Saved"
|
||||
preview: "Preview"
|
||||
home-profile: "Home profile"
|
||||
deck-profile: "Deck profile"
|
||||
room: "Room"
|
||||
@ -703,7 +704,7 @@ common/views/components/integration-settings.vue:
|
||||
title: "Service cooperation"
|
||||
connect: "Connect"
|
||||
disconnect: "Disconnect"
|
||||
connected-to: "You are connected to next account"
|
||||
connected-to: "You are connected to this account"
|
||||
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."
|
||||
connected-to: "You are connected to this GitHub account"
|
||||
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "Word mute"
|
||||
muted-words: "Muted keywords"
|
||||
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"
|
||||
common/views/components/password-settings.vue:
|
||||
reset: "Change password"
|
||||
@ -1276,7 +1279,9 @@ admin/views/instance.vue:
|
||||
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."
|
||||
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"
|
||||
remote-drive-capacity-mb: "Volume of Drive per remote user"
|
||||
mb: "In megabytes"
|
||||
@ -1352,9 +1357,9 @@ admin/views/charts.vue:
|
||||
charts:
|
||||
federation-instances: "The number of instances: increase/decrease"
|
||||
federation-instances-total: "Total number of instances"
|
||||
notes: "The number of posts: increase/decrease (Combined)"
|
||||
local-notes: "The number of posts: increase/decrease (Local)"
|
||||
remote-notes: "The number of posts: increase/decrease (Remote)"
|
||||
notes: "Increase, or decrease in the number of posts (Combined)"
|
||||
local-notes: "Increase, or decrease in the number of posts (Local)"
|
||||
remote-notes: "Increase, or decrease in the number of posts (Remote)"
|
||||
notes-total: "Total posts"
|
||||
users: "The number of users: increase/decrease"
|
||||
users-total: "Total users"
|
||||
@ -1533,7 +1538,7 @@ admin/views/federation.vue:
|
||||
chart-srcs:
|
||||
requests: "Requests"
|
||||
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-total: "Total number of notes"
|
||||
ff: "Increase of followers"
|
||||
@ -1989,6 +1994,9 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
round: "Round decimal"
|
||||
_round:
|
||||
arg1: "Number"
|
||||
eq: "A and B are equal"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
|
@ -16,6 +16,8 @@ common:
|
||||
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."
|
||||
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."
|
||||
close: "Cerrar"
|
||||
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"
|
||||
signup: "¡Regístrate!"
|
||||
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?"
|
||||
signin-required: "Inicie sesion"
|
||||
notification-type: "Tipo de notificación"
|
||||
notification-types:
|
||||
all: "Todo"
|
||||
pollVote: "Encuestas"
|
||||
follow: "Seguimientos"
|
||||
receiveFollowRequest: "Solicitudes de seguimiento"
|
||||
reply: "Responder"
|
||||
quote: "Citas"
|
||||
renote: "Volver a publicar"
|
||||
mention: "Menciones"
|
||||
reaction: "Reacciones"
|
||||
got-it: "¡Listo!"
|
||||
customization-tips:
|
||||
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!"
|
||||
notification:
|
||||
file-uploaded: "Archivo cargado."
|
||||
@ -73,20 +87,42 @@ common:
|
||||
"write:account": "Editar información de la cuenta"
|
||||
"read:blocks": "Ver bloques"
|
||||
"write:blocks": "Editar bloques"
|
||||
"read:drive": "Explorar el drive"
|
||||
"write:drive": "Administrar el drive"
|
||||
"read:favorites": "Ver favoritos"
|
||||
"write:favorites": "Editar favoritos"
|
||||
"read:following": "Ver información de seguidor"
|
||||
"write:following": "Seguir/Dejar de seguir"
|
||||
"read:messaging": "Ver conversación"
|
||||
"write:messaging": "Administrar coversación"
|
||||
"read:mutes": "Ver silenciados"
|
||||
"write:mutes": "Administrar silenciados"
|
||||
"write:notes": "Crear y eliminar articulos"
|
||||
"read:notifications": "Ver notificaciones"
|
||||
"write:notifications": "Administrar notificaciones"
|
||||
"read:reactions": "Ver reacciones"
|
||||
"write:reactions": "Administrar reacciones"
|
||||
"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:
|
||||
reply: "Responder"
|
||||
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"
|
||||
add-visible-user: "Agregar usuario"
|
||||
username-prompt: "Ingresar nombre de usuario"
|
||||
enter-file-name: "Editar nombre del archivo"
|
||||
weekday-short:
|
||||
sunday: "domingo"
|
||||
monday: "lunes"
|
||||
@ -141,15 +177,18 @@ common:
|
||||
mute-and-block: "Silenciar/Bloquear"
|
||||
blocking: "Bloquear"
|
||||
security: "Seguridad"
|
||||
signin: "Historial de ingresos"
|
||||
password: "Contraseña"
|
||||
other: "Otros"
|
||||
appearance: "Diseño"
|
||||
behavior: "Comportamiento"
|
||||
reactions: "Reacciones"
|
||||
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"
|
||||
default-note-visibility: "Rango de publicación predeterminado"
|
||||
web-search-engine: "Buscador web"
|
||||
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"
|
||||
use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
|
||||
line-width: "Grosor de línea"
|
||||
@ -201,6 +240,7 @@ common:
|
||||
navbar-position-left: "Izquierda"
|
||||
save: "Guardar"
|
||||
saved: "Guardado"
|
||||
preview: "Vista previa"
|
||||
search: "Buscar"
|
||||
delete: "eliminar"
|
||||
loading: "cargando"
|
||||
@ -868,6 +908,7 @@ desktop/views/components/settings.tags.vue:
|
||||
desktop/views/components/timeline.vue:
|
||||
home: "Inicio"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
list: "Listas"
|
||||
hashtag: "Hashtags"
|
||||
@ -1006,6 +1047,8 @@ admin/views/federation.vue:
|
||||
day: "Por día"
|
||||
blocked-hosts: "Bloquear"
|
||||
save: "Guardar"
|
||||
desktop/views/pages/welcome.vue:
|
||||
timeline: "Timeline"
|
||||
desktop/views/pages/selectdrive.vue:
|
||||
cancel: "Cancelar"
|
||||
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:
|
||||
month: "lunes"
|
||||
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:
|
||||
title: "Notificaciones"
|
||||
desktop/views/widgets/polls.vue:
|
||||
@ -1066,6 +1114,7 @@ mobile/views/components/ui.header.vue:
|
||||
welcome-back: "Bienvenido/a de vuelta,"
|
||||
adjective: "-san"
|
||||
mobile/views/components/ui.nav.vue:
|
||||
timeline: "Timeline"
|
||||
notifications: "Notificaciones"
|
||||
follow-requests: "Solicitudes de seguimiento"
|
||||
search: "Buscar"
|
||||
@ -1080,6 +1129,7 @@ mobile/views/pages/drive.vue:
|
||||
mobile/views/pages/home.vue:
|
||||
home: "Inicio"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
global: "Global"
|
||||
mobile/views/pages/widgets.vue:
|
||||
dashboard: "Panel de control"
|
||||
@ -1093,6 +1143,8 @@ mobile/views/pages/search.vue:
|
||||
search: "Buscar"
|
||||
mobile/views/pages/notifications.vue:
|
||||
notifications: "Notificaciones"
|
||||
mobile/views/pages/user.vue:
|
||||
timeline: "Timeline"
|
||||
mobile/views/pages/user/home.vue:
|
||||
activity: "Actividad"
|
||||
mobile/views/pages/user/home.photos.vue:
|
||||
@ -1100,6 +1152,7 @@ mobile/views/pages/user/home.photos.vue:
|
||||
deck:
|
||||
home: "Inicio"
|
||||
local: "Local"
|
||||
hybrid: "Social"
|
||||
hashtag: "Etiquetas"
|
||||
global: "Global"
|
||||
notifications: "Notificaciones"
|
||||
@ -1107,6 +1160,7 @@ deck:
|
||||
rename: "Renombrar"
|
||||
deck/deck.user-column.vue:
|
||||
activity: "Actividad"
|
||||
timeline: "Timeline"
|
||||
pages:
|
||||
pin-this-page: "Fijar en el perfil"
|
||||
like: "Me gusta"
|
||||
|
@ -31,6 +31,7 @@ common:
|
||||
signup: "S'enregistrer"
|
||||
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 ?"
|
||||
fetching-as-ap-object: "Récupération depuis le fédiverse"
|
||||
unfollow-confirm: "Désirez-vous vous désabonner de {name} ?"
|
||||
delete-confirm: "Supprimer cette publication ?"
|
||||
signin-required: "Veuillez vous connecter"
|
||||
@ -48,6 +49,7 @@ common:
|
||||
got-it: "J’ai compris !"
|
||||
customization-tips:
|
||||
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 !"
|
||||
notification:
|
||||
file-uploaded: "Le fichier a été téléversé !"
|
||||
@ -90,9 +92,11 @@ common:
|
||||
"read:favorites": "Afficher les favoris"
|
||||
"write:favorites": "Écrire des favoris"
|
||||
"read:following": "Voir les informations de l'abonné"
|
||||
"write:following": "Suivre/Ne plus suivre"
|
||||
"read:messaging": "Lire les conversations"
|
||||
"write:messaging": "Utiliser la messagerie"
|
||||
"read:mutes": "Voir les comptes masqués"
|
||||
"write:mutes": "Gérer les comptes muets"
|
||||
"write:notes": "Créer ou supprimer des publications"
|
||||
"read:notifications": "Afficher les notifications"
|
||||
"write:notifications": "Gérer vos notifications"
|
||||
@ -100,6 +104,11 @@ common:
|
||||
"write:reactions": "Gérer vos réactions"
|
||||
"write:votes": "Vote"
|
||||
"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:
|
||||
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"
|
||||
@ -110,6 +119,7 @@ common:
|
||||
quote-placeholder: "Citer cette note …"
|
||||
option-quote-placeholder: "Citer ce billet ... (Facultatif)"
|
||||
quote-attached: "Cité"
|
||||
quote-question: "Souhaitez-vous ajoutez une citation ?"
|
||||
submit: "Publication"
|
||||
reply: "Répondre"
|
||||
renote: "Republier"
|
||||
@ -126,6 +136,7 @@ common:
|
||||
geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation"
|
||||
error: "Erreur"
|
||||
enter-username: "Saisir un nom d'utilisateur"
|
||||
specified-recipient: "Correspondant·e"
|
||||
add-visible-user: "Ajouter un utilisateur"
|
||||
cw-placeholder: "Commenter le contenu (optionnel)"
|
||||
username-prompt: "Saisir un nom d'utilisateur"
|
||||
@ -190,6 +201,7 @@ common:
|
||||
appearance: "Apparence"
|
||||
behavior: "Comportement"
|
||||
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-desc: "Chargement automatique du contenu lors du défilement de la page."
|
||||
note-visibility: "Visibilité de la publication"
|
||||
@ -198,7 +210,9 @@ common:
|
||||
web-search-engine: "Moteur de recherche Web"
|
||||
web-search-engine-desc: "Exemple : https://www.google.com/?#q={{query}}"
|
||||
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\""
|
||||
paste-dialog: "Modifier le nom du fichier collé"
|
||||
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."
|
||||
i-like-sushi: "Je préfère les sushis plutôt que le pudding"
|
||||
@ -206,6 +220,7 @@ common:
|
||||
use-avatar-reversi-stones: "Utiliser l’avatar comme pion dans Reversi"
|
||||
disable-animated-mfm: "Désactiver les textes animés dans les publications"
|
||||
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"
|
||||
always-show-nsfw: "Toujours afficher les contenus sensibles"
|
||||
always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles"
|
||||
@ -284,6 +299,7 @@ common:
|
||||
sync: "Synchroniser"
|
||||
save: "Enregistrer"
|
||||
saved: "enregistré"
|
||||
preview: "Prévisualisation"
|
||||
home-profile: "Profil principal"
|
||||
deck-profile: "Profil deck"
|
||||
room: "Pièce"
|
||||
@ -376,9 +392,11 @@ common/views/pages/explore.vue:
|
||||
popular-users: "Utilisateur·rice·s populaires"
|
||||
recently-updated-users: "Utilisateur·rice·s actif·ve·s récemment"
|
||||
recently-registered-users: "Les nouveaux inscrits"
|
||||
recently-discovered-users: "Utilisateurs récemment découverts"
|
||||
popular-tags: "Mots-clés populaires"
|
||||
federated: "Du Fédiverse"
|
||||
explore: "Explorer {host}"
|
||||
explore-fediverse: "Explorer le Fédiverse"
|
||||
users-info: "Actuellement, {users} utilisateur·rice·s se sont inscrit ici"
|
||||
common/views/components/reactions-viewer.details.vue:
|
||||
few-users: "{users} ont réagit avec {reaction}"
|
||||
@ -548,6 +566,7 @@ common/views/components/note-menu.vue:
|
||||
delete: "Supprimer"
|
||||
delete-confirm: "Supprimer cette publication ?"
|
||||
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"
|
||||
pin-limit-exceeded: "Vous ne pouvez plus épingler davantage de publications."
|
||||
common/views/components/user-menu.vue:
|
||||
@ -571,6 +590,7 @@ common/views/components/user-menu.vue:
|
||||
suspend: "Suspendre"
|
||||
unsuspend: "Ne plus suspendre"
|
||||
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:
|
||||
vote-to: "Voter pour '{}'"
|
||||
vote-count: "{} votes"
|
||||
@ -593,6 +613,7 @@ common/views/components/poll-editor.vue:
|
||||
expiration: "Valide jusqu'à"
|
||||
infinite: "Illimité"
|
||||
at: "Choisir une date et une durée"
|
||||
after: "Choisir la durée"
|
||||
no-more: "Vous ne pouvez pas en ajouter davantage"
|
||||
deadline-date: "Date d’échéance"
|
||||
deadline-time: "Durée"
|
||||
@ -606,7 +627,9 @@ common/views/components/reaction-picker.vue:
|
||||
choose-reaction: "Envoyer une réaction"
|
||||
input-reaction-placeholder: "ou insérez un émoji"
|
||||
common/views/components/emoji-picker.vue:
|
||||
recent-emoji: "Utilisés récemment"
|
||||
custom-emoji: "Émoji personnalisé"
|
||||
no-category: "Sans catégorie"
|
||||
people: "Personnes"
|
||||
animals-and-nature: "Animaux et nature"
|
||||
food-and-drink: "Nourriture et boisson"
|
||||
@ -802,6 +825,7 @@ common/views/widgets/broadcast.vue:
|
||||
no-broadcasts: "Aucune annonce"
|
||||
have-a-nice-day: "Passez une bonne journée !"
|
||||
next: "Suivant"
|
||||
prev: "Précédent"
|
||||
common/views/widgets/calendar.vue:
|
||||
year: "Année {}"
|
||||
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-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-line23: "Ai-chan kawaii!"
|
||||
tips-line24: "Misskey est fonctionnel depuis 2014"
|
||||
tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible"
|
||||
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."
|
||||
totp-header: "Application d'authentification"
|
||||
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 :"
|
||||
activate-key: "Cliquez pour activer la clé de sécurité"
|
||||
security-key-name: "Nom de la clé"
|
||||
@ -1086,6 +1112,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "Filtre de mots"
|
||||
muted-words: "Mots masqués"
|
||||
muted-words-description: "Description des mots mis en sourdine"
|
||||
unmute-confirm: "Ne plus masquer cet utilisateur ?"
|
||||
unblock-confirm: "Débloquer cet utilisateur ?"
|
||||
save: "Enregistrer"
|
||||
common/views/components/password-settings.vue:
|
||||
reset: "Modifier le mot de passe"
|
||||
@ -1167,6 +1195,8 @@ admin/views/index.vue:
|
||||
back-to-misskey: "Retour vers Misskey"
|
||||
admin/views/db.vue:
|
||||
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:
|
||||
dashboard: "Tableau de bord"
|
||||
accounts: "Comptes"
|
||||
@ -1269,6 +1299,7 @@ admin/views/instance.vue:
|
||||
discord-integration-client-id: "ID client"
|
||||
discord-integration-client-secret: "Secret client"
|
||||
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 d’utilisateur du compte proxy"
|
||||
proxy-account-username-desc: "Spécifiez le nom d’utilisateur du compte utilisé comme proxy."
|
||||
proxy-account-warn: "Avant d’entamer cette action, vous devez au préalable avoir créé un compte avec ce nom d’utilisateur."
|
||||
@ -1350,6 +1381,7 @@ admin/views/drive.vue:
|
||||
marked-as-sensitive: "Marqué comme sensible"
|
||||
unmarked-as-sensitive: "Marqué comme non sensible"
|
||||
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"
|
||||
admin/views/users.vue:
|
||||
operation: "Actions"
|
||||
@ -1416,6 +1448,7 @@ admin/views/emoji.vue:
|
||||
title: "Ajouter un émoji"
|
||||
name: "Nom de l’émoji"
|
||||
name-desc: "Vous pouvez utiliser les caractères a~z 0~9 _"
|
||||
category: "Catégories"
|
||||
aliases: "Aliases"
|
||||
aliases-desc: "Vous pouvez définir plus d’un, séparés par des espaces."
|
||||
url: "URL de l’image"
|
||||
@ -1488,6 +1521,7 @@ admin/views/federation.vue:
|
||||
notes: "Augmentation/diminution du nombre des notes"
|
||||
notes-total: "Nombre total des notes"
|
||||
ff: "Augmentation des abonné·e·s"
|
||||
ff-total: "Nombre total d'abonnements"
|
||||
drive-usage: "Augmentation et diminution de la capacité stockage"
|
||||
drive-usage-total: "Utilisation totale du stockage"
|
||||
drive-files-total: "Nombre total des fichiers sur le Drive"
|
||||
@ -1881,9 +1915,11 @@ pages:
|
||||
value: "Valeur"
|
||||
fn: "Fonction"
|
||||
text: "Actions texte"
|
||||
convert: "Convertir"
|
||||
list: "Listes"
|
||||
blocks:
|
||||
text: "Texte"
|
||||
multiLineText: "Texte (Multi-lignes)"
|
||||
textList: "Liste de texte"
|
||||
strLen: "Longueur du texte"
|
||||
_strLen:
|
||||
@ -1891,6 +1927,8 @@ pages:
|
||||
strPick: "Extraire un caractère"
|
||||
_strPick:
|
||||
arg1: "Texte"
|
||||
arg2: "Position du joueur"
|
||||
strReplace: "Remplacement de texte"
|
||||
_strReplace:
|
||||
arg1: "Texte"
|
||||
arg2: "Avant le remplacement"
|
||||
@ -1920,6 +1958,8 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
_round:
|
||||
arg1: "Numérique"
|
||||
eq: "A et B sont équivalents"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
@ -1952,6 +1992,7 @@ pages:
|
||||
_gtEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
if: "Branche"
|
||||
_if:
|
||||
arg1: "Si"
|
||||
arg2: "donc"
|
||||
@ -1969,6 +2010,7 @@ pages:
|
||||
randomPick: "Choisir aléatoirement depuis la liste"
|
||||
_randomPick:
|
||||
arg1: "Listes"
|
||||
dailyRandom: "Aléatoire (Quotidien pour chaque utilisateur)"
|
||||
_dailyRandom:
|
||||
arg1: "Probabilité"
|
||||
_dailyRannum:
|
||||
@ -1976,19 +2018,27 @@ pages:
|
||||
arg2: "Maximum"
|
||||
_dailyRandomPick:
|
||||
arg1: "Listes"
|
||||
seedRandom: "Aléatoire (graine)"
|
||||
_seedRandom:
|
||||
arg1: "Graine"
|
||||
arg2: "Probabilité"
|
||||
seedRannum: "Nombre aléatoire (Graine)"
|
||||
_seedRannum:
|
||||
arg1: "Graine"
|
||||
arg2: "Min"
|
||||
arg3: "Max"
|
||||
seedRandomPick: "Sélection aléatoire dans une liste (Graine)"
|
||||
_seedRandomPick:
|
||||
arg1: "Graine"
|
||||
arg2: "Listes"
|
||||
DRPWPM: "Sélection aléatoire à partir d'une liste pondérée (mise à jour quotidienne par utilisateur)"
|
||||
_DRPWPM:
|
||||
arg1: "Liste de texte"
|
||||
pick: "Sélectionner dans la liste"
|
||||
_pick:
|
||||
arg1: "Listes"
|
||||
arg2: "Position"
|
||||
listLen: "Longueur de la liste"
|
||||
_listLen:
|
||||
arg1: "Listes"
|
||||
number: "Numérique"
|
||||
@ -1998,12 +2048,14 @@ pages:
|
||||
numberToString: "Chiffres en chaîne"
|
||||
_numberToString:
|
||||
arg1: "Numérique"
|
||||
splitStrByLine: "Séparer le texte par lignes"
|
||||
_splitStrByLine:
|
||||
arg1: "Texte"
|
||||
ref: "Variables"
|
||||
fn: "Fonction"
|
||||
_fn:
|
||||
slots: "Emplacement"
|
||||
slots-info: "Veuillez délimiter chaque emplacement par un saut de ligne"
|
||||
arg1: "Sortie"
|
||||
for: "Répéter"
|
||||
_for:
|
||||
@ -2019,10 +2071,12 @@ pages:
|
||||
emptySlot: "Slot vide"
|
||||
enviromentVariables: "Variables d'environnement"
|
||||
pageVariables: "Élément de page"
|
||||
argVariables: "Entrée vide"
|
||||
room:
|
||||
add-furniture: "Placer des meubles"
|
||||
translate: "Déplacer"
|
||||
rotate: "Tourner"
|
||||
exit: "Retour"
|
||||
remove: "Enlever"
|
||||
save: "Enregistrer"
|
||||
saved: "enregistré"
|
||||
@ -2048,6 +2102,7 @@ room:
|
||||
plant2: "Plante d’intérieur 2"
|
||||
eraser: "Gomme"
|
||||
pencil: "Crayon"
|
||||
pudding: "Pudding"
|
||||
cardboard-box: "Boîte en carton"
|
||||
cardboard-box2: "Boîte en carton 2"
|
||||
cardboard-box3: "Boîte en carton 3"
|
||||
@ -2062,6 +2117,7 @@ room:
|
||||
monitor: "Écran"
|
||||
keyboard: "Clavier"
|
||||
carpet-stripe: "Tapis (zébré)"
|
||||
mat: "Tapis"
|
||||
color-box: "Étagère"
|
||||
wall-clock: "Horloge murale"
|
||||
photoframe: "Cadre photo"
|
||||
|
@ -1225,6 +1225,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "ワードミュート"
|
||||
muted-words: "ミュートされたキーワード"
|
||||
muted-words-description: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
||||
unmute-confirm: "このユーザーをミュート解除しますか?"
|
||||
unblock-confirm: "このユーザーをブロック解除しますか?"
|
||||
save: "保存"
|
||||
|
||||
common/views/components/password-settings.vue:
|
||||
@ -1408,7 +1410,9 @@ admin/views/instance.vue:
|
||||
object-storage-s3-info-here: "こちら"
|
||||
object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。"
|
||||
cache-remote-files: "リモートのファイルをキャッシュする"
|
||||
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。"
|
||||
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにするか次のリモートファイルのプロキシを有効にすることをおすすめします。"
|
||||
proxy-remote-files: "リモートのファイルをプロキシする"
|
||||
proxy-remote-files-desc: "この設定を有効にすると、未保存または保存容量超過で削除されたリモートファイルをローカルでプロキシし、サムネイルも生成するようになります。"
|
||||
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
|
||||
remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
|
||||
mb: "メガバイト単位"
|
||||
@ -2198,6 +2202,9 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
round: "小数を丸める"
|
||||
_round:
|
||||
arg1: "数値"
|
||||
eq: "AとBが同じ"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
|
@ -130,6 +130,7 @@ common:
|
||||
timeline: "タイムライン"
|
||||
save: "保存"
|
||||
saved: "保存したで!"
|
||||
preview: "試してみる"
|
||||
search: "検索"
|
||||
delete: "削除"
|
||||
loading: "読み込み中"
|
||||
@ -866,7 +867,6 @@ admin/views/instance.vue:
|
||||
drive-config: "ドライブの設定"
|
||||
object-storage-endpoint: "エンドポイント"
|
||||
cache-remote-files: "リモートのファイルをキャッシュする"
|
||||
cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。"
|
||||
local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
|
||||
remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
|
||||
mb: "メガバイト単位"
|
||||
|
@ -300,6 +300,7 @@ common:
|
||||
sync: "동기화"
|
||||
save: "저장"
|
||||
saved: "저장하였습니다"
|
||||
preview: "미리보기"
|
||||
home-profile: "홈 프로필"
|
||||
deck-profile: "덱 프로필"
|
||||
room: "룸"
|
||||
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "단어 뮤트"
|
||||
muted-words: "뮤트된 키워드"
|
||||
muted-words-description: "공백으로 구분하는 경우 AND로 지정되며, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
|
||||
unmute-confirm: "이 사용자를 뮤트 해제하시겠습니까?"
|
||||
unblock-confirm: "이 사용자를 차단 해제하시겠습니까?"
|
||||
save: "저장"
|
||||
common/views/components/password-settings.vue:
|
||||
reset: "비밀번호 변경"
|
||||
@ -1276,7 +1279,9 @@ admin/views/instance.vue:
|
||||
object-storage-s3-info-here: "이곳"
|
||||
object-storage-gcs-info: "Google Cloud Storage를 오브젝트 스토리지로 사용하는 경우, 「엔드포인트」는 storage.googleapis.com 으로 설정하고, 「리전」 란은 비웁니다."
|
||||
cache-remote-files: "원격 파일을 캐시"
|
||||
cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다."
|
||||
cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 보통은 이 설정을 사용하거나 아래의 원격 파일 프록시를 설정하는 것을 추천합니다."
|
||||
proxy-remote-files: "원격 파일 프록시"
|
||||
proxy-remote-files-desc: "이 설정을 사용하면, 저장되지 않았거나 용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다."
|
||||
local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량"
|
||||
remote-drive-capacity-mb: "원격 사용자 한 명당 드라이브 용량"
|
||||
mb: "메가바이트 단위"
|
||||
@ -1989,6 +1994,9 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
round: "소수점을 반올림"
|
||||
_round:
|
||||
arg1: "수치"
|
||||
eq: "A와 B가 동일"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
|
@ -162,6 +162,7 @@ common:
|
||||
note-visibility: "Widoczność wpisów"
|
||||
remember-note-visibility: "Zapamiętaj widoczność wpisów"
|
||||
web-search-engine: "Wyszukiwarka internetowa"
|
||||
web-search-engine-desc: "Wzór: https://www.google.com/?#q={{query}}"
|
||||
paste: "Wklej"
|
||||
line-width: "Szerokości linii"
|
||||
line-width-thin: "Cienka"
|
||||
@ -193,6 +194,7 @@ common:
|
||||
navbar-position-left: "Z lewej"
|
||||
save: "Zapisz"
|
||||
saved: "Zapisano"
|
||||
preview: "Pokaż podgląd"
|
||||
search: "Szukaj"
|
||||
delete: "Usuń"
|
||||
loading: "Ładowanie"
|
||||
|
@ -12,7 +12,7 @@ common:
|
||||
rich-contents: "Посты"
|
||||
rich-contents-desc: "Просто выложи свою идею, актуальные темы и всё, что тебе хочется показать миру. Ты можешь декорировать свои слова, прикреплять свои любимые картинки, отправлять файлы с фильмами и создать голосование - это те вещи, которые ты можешь сделать с помощью Misskey!"
|
||||
reaction: "Реакции"
|
||||
reaction-desc: "Самый лёгкий способ выразить свои эмоции. Misskey позволяет добавлять различные виды реакций к постам других людей. Эмоциональный опыт из Misskey никогда не появится в других социальных сетях, позволяющих только жать “лайки”."
|
||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
ui: "Интерфейс"
|
||||
ui-desc: "Нет такого интерфейса, понравившегося всем. Поэтому у Misskey имеется пользовательский интерфейс, широко настраиваемый под ваши вкусы. Создай себе уникальную домашнюю страницу редактируя, подстраивая оформление ленты и размещая виджеты, которые тоже можно кастомизировать."
|
||||
drive: "Хранилище файлов"
|
||||
|
@ -300,6 +300,7 @@ common:
|
||||
sync: "同步"
|
||||
save: "保存"
|
||||
saved: "已保存"
|
||||
preview: "预览"
|
||||
home-profile: "定制首页数据"
|
||||
deck-profile: "定制Deck数据"
|
||||
room: "房间"
|
||||
@ -363,7 +364,7 @@ common:
|
||||
notifications: "通知"
|
||||
users: "推荐用户"
|
||||
polls: "调查问卷"
|
||||
post-form: "投稿形式"
|
||||
post-form: "投稿窗口"
|
||||
server: "服务器信息"
|
||||
nav: "导航"
|
||||
tips: "提示"
|
||||
@ -1115,6 +1116,8 @@ common/views/components/mute-and-block.vue:
|
||||
word-mute: "文字屏蔽"
|
||||
muted-words: "屏蔽关键字"
|
||||
muted-words-description: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范"
|
||||
unmute-confirm: "取消屏蔽用户?"
|
||||
unblock-confirm: "取消拉黑此用户?"
|
||||
save: "保存"
|
||||
common/views/components/password-settings.vue:
|
||||
reset: "更改密码"
|
||||
@ -1276,7 +1279,7 @@ admin/views/instance.vue:
|
||||
object-storage-s3-info-here: "这里"
|
||||
object-storage-gcs-info: "将Google Cloud Storage用作对象存储时,请将“终端”设置为storage.googleapis.com,并将“区域”留空。"
|
||||
cache-remote-files: "远程文件缓存"
|
||||
cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。"
|
||||
proxy-remote-files: "代理远程文件"
|
||||
local-drive-capacity-mb: "每个用户的网盘空间"
|
||||
remote-drive-capacity-mb: "每个远程用户的网盘容量"
|
||||
mb: "以兆字节(Mbps)为单位"
|
||||
@ -1880,10 +1883,10 @@ pages:
|
||||
section: "章节"
|
||||
image: "图片"
|
||||
button: "按钮"
|
||||
if: "如果"
|
||||
if: "判断"
|
||||
_if:
|
||||
variable: "变量"
|
||||
post: "投稿形式"
|
||||
post: "投稿窗口"
|
||||
_post:
|
||||
text: "内容"
|
||||
textInput: "文本输入"
|
||||
@ -1989,6 +1992,9 @@ pages:
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
round: "四舍五入"
|
||||
_round:
|
||||
arg1: "数值"
|
||||
eq: "A和B相等"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
|
14
migration/1576269851876-TalkFederationId.ts
Normal file
14
migration/1576269851876-TalkFederationId.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
14
migration/1576869585998-ProxyRemoteFiles.ts
Normal file
14
migration/1576869585998-ProxyRemoteFiles.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
183
package.json
183
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "11.35.1",
|
||||
"version": "11.37.1",
|
||||
"codename": "daybreak",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -26,52 +26,48 @@
|
||||
"format": "gulp format"
|
||||
},
|
||||
"resolutions": {
|
||||
"gulp-cssnano/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1",
|
||||
"https-proxy-agent": "^3.0.0",
|
||||
"lodash": "^4.17.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "5.11.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.11.2",
|
||||
"@fortawesome/vue-fontawesome": "0.1.7",
|
||||
"@elastic/elasticsearch": "7.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "5.12.0",
|
||||
"@fortawesome/free-regular-svg-icons": "5.12.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.12.0",
|
||||
"@fortawesome/vue-fontawesome": "0.1.9",
|
||||
"@koa/cors": "3.0.0",
|
||||
"@koa/multer": "2.0.0",
|
||||
"@koa/router": "8.0.2",
|
||||
"@koa/multer": "2.0.2",
|
||||
"@koa/router": "8.0.5",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.10.3",
|
||||
"@types/cbor": "2.0.0",
|
||||
"@types/bull": "3.10.6",
|
||||
"@types/cbor": "5.0.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/double-ended-queue": "2.1.1",
|
||||
"@types/gulp": "4.0.6",
|
||||
"@types/gulp-mocha": "0.0.32",
|
||||
"@types/gulp-rename": "0.0.33",
|
||||
"@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/js-yaml": "3.12.1",
|
||||
"@types/jsdom": "12.2.4",
|
||||
"@types/katex": "0.10.2",
|
||||
"@types/koa": "2.0.50",
|
||||
"@types/koa-bodyparser": "5.0.2",
|
||||
"@types/katex": "0.11.0",
|
||||
"@types/koa": "2.11.0",
|
||||
"@types/koa-bodyparser": "4.3.0",
|
||||
"@types/koa-compress": "2.0.9",
|
||||
"@types/koa-cors": "0.0.0",
|
||||
"@types/koa-favicon": "2.0.19",
|
||||
"@types/koa-logger": "3.1.1",
|
||||
"@types/koa-mount": "4.0.0",
|
||||
"@types/koa-send": "4.1.2",
|
||||
"@types/koa-views": "2.0.3",
|
||||
"@types/koa__cors": "2.2.3",
|
||||
"@types/koa__multer": "2.0.0",
|
||||
"@types/koa__router": "8.0.0",
|
||||
"@types/lolex": "3.1.1",
|
||||
"@types/koa-views": "2.0.4",
|
||||
"@types/koa__cors": "3.0.0",
|
||||
"@types/koa__multer": "2.0.1",
|
||||
"@types/koa__router": "8.0.2",
|
||||
"@types/lolex": "5.1.0",
|
||||
"@types/mocha": "5.2.7",
|
||||
"@types/node": "12.7.12",
|
||||
"@types/nodemailer": "6.2.1",
|
||||
"@types/node": "13.1.4",
|
||||
"@types/nodemailer": "6.4.0",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "5.0.2",
|
||||
@ -83,80 +79,78 @@
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.8.14",
|
||||
"@types/rename": "1.0.1",
|
||||
"@types/request": "2.48.3",
|
||||
"@types/request": "2.48.4",
|
||||
"@types/request-promise-native": "1.0.17",
|
||||
"@types/request-stats": "3.0.0",
|
||||
"@types/rimraf": "2.0.2",
|
||||
"@types/rimraf": "2.0.3",
|
||||
"@types/seedrandom": "2.4.28",
|
||||
"@types/sharp": "0.22.3",
|
||||
"@types/sharp": "0.23.1",
|
||||
"@types/showdown": "1.9.3",
|
||||
"@types/speakeasy": "2.0.5",
|
||||
"@types/systeminformation": "3.23.1",
|
||||
"@types/systeminformation": "3.54.1",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"@types/tmp": "0.1.0",
|
||||
"@types/uuid": "3.4.5",
|
||||
"@types/uuid": "3.4.6",
|
||||
"@types/web-push": "3.3.0",
|
||||
"@types/webpack": "4.39.3",
|
||||
"@types/webpack": "4.41.1",
|
||||
"@types/webpack-stream": "3.2.10",
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.3",
|
||||
"@typescript-eslint/parser": "2.3.3",
|
||||
"@types/websocket": "1.0.0",
|
||||
"@types/ws": "6.0.4",
|
||||
"@typescript-eslint/parser": "2.15.0",
|
||||
"agentkeepalive": "4.1.0",
|
||||
"animejs": "3.1.0",
|
||||
"apexcharts": "3.10.1",
|
||||
"apexcharts": "3.12.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.548.0",
|
||||
"aws-sdk": "2.598.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bootstrap": "4.3.1",
|
||||
"bootstrap-vue": "2.0.4",
|
||||
"bull": "3.11.0",
|
||||
"cafy": "15.1.1",
|
||||
"bootstrap": "4.4.1",
|
||||
"bootstrap-vue": "2.1.0",
|
||||
"bull": "3.12.1",
|
||||
"cafy": "15.2.1",
|
||||
"cbor": "5.0.1",
|
||||
"chai": "4.2.0",
|
||||
"chalk": "2.4.2",
|
||||
"cli-highlight": "2.1.1",
|
||||
"commander": "3.0.2",
|
||||
"chalk": "3.0.0",
|
||||
"cli-highlight": "2.1.4",
|
||||
"commander": "4.1.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "3.2.0",
|
||||
"css-loader": "3.4.1",
|
||||
"cssnano": "4.1.10",
|
||||
"dateformat": "3.0.3",
|
||||
"deep-equal": "1.1.0",
|
||||
"diskusage": "1.1.3",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"eslint": "6.5.1",
|
||||
"eslint-plugin-vue": "5.2.3",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-plugin-vue": "6.1.2",
|
||||
"eventemitter3": "4.0.0",
|
||||
"feed": "4.0.0",
|
||||
"file-type": "12.3.0",
|
||||
"feed": "4.1.0",
|
||||
"file-type": "13.0.1",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-clean-css": "4.2.0",
|
||||
"gulp-mocha": "7.0.2",
|
||||
"gulp-rename": "1.4.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.0.0",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-stylus": "2.7.0",
|
||||
"gulp-terser": "1.2.0",
|
||||
"gulp-tslint": "8.1.4",
|
||||
"gulp-typescript": "5.0.1",
|
||||
"gulp-uglify": "3.0.2",
|
||||
"gulp-util": "3.0.8",
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
"html-minifier": "4.0.0",
|
||||
"http-signature": "1.2.0",
|
||||
"https-proxy-agent": "3.0.0",
|
||||
"http-signature": "1.3.1",
|
||||
"https-proxy-agent": "4.0.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-root": "2.1.0",
|
||||
"is-svg": "4.2.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsdom": "15.1.1",
|
||||
"jsdom": "15.2.1",
|
||||
"json5": "2.1.1",
|
||||
"json5-loader": "3.0.0",
|
||||
"jsrsasign": "8.0.12",
|
||||
"katex": "0.11.1",
|
||||
"koa": "2.10.0",
|
||||
"koa": "2.11.0",
|
||||
"koa-bodyparser": "4.2.1",
|
||||
"koa-compress": "3.0.0",
|
||||
"koa-favicon": "2.0.1",
|
||||
@ -168,34 +162,34 @@
|
||||
"koa-views": "6.2.1",
|
||||
"langmap": "0.0.16",
|
||||
"loader-utils": "1.2.3",
|
||||
"lolex": "4.2.0",
|
||||
"lolex": "5.1.2",
|
||||
"lookup-dns-cache": "2.1.0",
|
||||
"mocha": "6.2.1",
|
||||
"mocha": "7.0.0",
|
||||
"moji": "0.5.1",
|
||||
"ms": "2.1.2",
|
||||
"multer": "1.4.2",
|
||||
"nested-property": "1.0.1",
|
||||
"nested-property": "1.0.2",
|
||||
"node-fetch": "2.6.0",
|
||||
"nodemailer": "6.3.1",
|
||||
"nodemailer": "6.4.2",
|
||||
"nprogress": "0.2.0",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.1.0",
|
||||
"parse5": "5.1.1",
|
||||
"parsimmon": "1.13.0",
|
||||
"pg": "7.12.1",
|
||||
"pg": "7.17.0",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"prismjs": "1.17.1",
|
||||
"prismjs": "1.18.0",
|
||||
"progress-bar-webpack-plugin": "1.12.1",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
"pug": "2.0.4",
|
||||
"punycode": "2.1.1",
|
||||
"pureimage": "0.1.6",
|
||||
"qrcode": "1.4.2",
|
||||
"qrcode": "1.4.4",
|
||||
"random-seed": "0.3.0",
|
||||
"randomcolor": "0.5.4",
|
||||
"ratelimiter": "3.3.1",
|
||||
"ratelimiter": "3.4.0",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "4.2.0",
|
||||
"redis": "2.8.0",
|
||||
@ -203,69 +197,68 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"request": "2.88.0",
|
||||
"request-promise-native": "1.0.7",
|
||||
"request-promise-native": "1.0.8",
|
||||
"request-stats": "3.0.0",
|
||||
"require-all": "3.0.0",
|
||||
"rimraf": "3.0.0",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.23.1",
|
||||
"showdown": "1.9.0",
|
||||
"sharp": "0.23.4",
|
||||
"showdown": "1.9.1",
|
||||
"showdown-highlightjs-extension": "0.1.2",
|
||||
"speakeasy": "2.0.0",
|
||||
"stringz": "2.0.0",
|
||||
"style-loader": "1.0.0",
|
||||
"style-loader": "1.1.2",
|
||||
"stylus": "0.54.7",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.3.1",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "4.14.11",
|
||||
"systeminformation": "4.17.3",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"terser-webpack-plugin": "2.1.3",
|
||||
"terser-webpack-plugin": "2.3.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.109.0",
|
||||
"three": "0.112.1",
|
||||
"tinycolor2": "1.4.1",
|
||||
"tmp": "0.1.0",
|
||||
"ts-loader": "6.2.0",
|
||||
"ts-node": "8.4.1",
|
||||
"tslint": "5.20.0",
|
||||
"ts-loader": "6.2.1",
|
||||
"ts-node": "8.5.4",
|
||||
"tslint": "5.20.1",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"typeorm": "0.2.19",
|
||||
"typescript": "3.6.4",
|
||||
"uglify-es": "3.3.9",
|
||||
"typeorm": "0.2.22",
|
||||
"typescript": "3.7.4",
|
||||
"ulid": "2.3.0",
|
||||
"url-loader": "2.2.0",
|
||||
"url-loader": "3.0.0",
|
||||
"uuid": "3.3.3",
|
||||
"v-animate-css": "0.0.3",
|
||||
"v-debounce": "0.1.2",
|
||||
"vue": "2.6.10",
|
||||
"vue": "2.6.11",
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.6.0",
|
||||
"vue-cropperjs": "4.0.0",
|
||||
"vue-i18n": "8.14.1",
|
||||
"vue-cropperjs": "4.0.1",
|
||||
"vue-i18n": "8.15.3",
|
||||
"vue-js-modal": "1.3.31",
|
||||
"vue-json-pretty": "1.6.2",
|
||||
"vue-loader": "15.7.1",
|
||||
"vue-json-pretty": "1.6.3",
|
||||
"vue-loader": "15.8.3",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-prism-component": "1.1.1",
|
||||
"vue-router": "3.1.3",
|
||||
"vue-sequential-entrance": "1.1.3",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader": "1.3.3",
|
||||
"vue-template-compiler": "2.6.10",
|
||||
"vue-svg-inline-loader": "1.4.4",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuedraggable": "2.23.2",
|
||||
"vuewordcloud": "18.7.11",
|
||||
"vuex": "3.1.1",
|
||||
"vuex-persistedstate": "2.5.4",
|
||||
"web-push": "3.4.0",
|
||||
"webpack": "4.41.1",
|
||||
"webpack-cli": "3.3.9",
|
||||
"websocket": "1.0.30",
|
||||
"ws": "7.1.2",
|
||||
"vuex": "3.1.2",
|
||||
"vuex-persistedstate": "2.7.0",
|
||||
"web-push": "3.4.3",
|
||||
"webpack": "4.41.5",
|
||||
"webpack-cli": "3.3.10",
|
||||
"websocket": "1.0.31",
|
||||
"ws": "7.2.1",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "2.1.10"
|
||||
"@types/fluent-ffmpeg": "2.1.12"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as cluster from 'cluster';
|
||||
import chalk from 'chalk';
|
||||
import * as chalk from 'chalk';
|
||||
import Xev from 'xev';
|
||||
|
||||
import Logger from '../services/logger';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as os from 'os';
|
||||
import * as cluster from 'cluster';
|
||||
import chalk from 'chalk';
|
||||
import * as chalk from 'chalk';
|
||||
import * as portscanner from 'portscanner';
|
||||
import * as isRoot from 'is-root';
|
||||
|
||||
@ -11,14 +11,15 @@ import { lessThan } from '../prelude/array';
|
||||
import { program } from '../argv';
|
||||
import { showMachineInfo } from '../misc/show-machine-info';
|
||||
import { initDb } from '../db/postgre';
|
||||
import * as meta from '../meta.json';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
|
||||
function greet(config: Config) {
|
||||
function greet() {
|
||||
if (!program.quiet) {
|
||||
//#region Misskey logo
|
||||
const v = `v${config.version}`;
|
||||
const v = `v${meta.version}`;
|
||||
console.log(' _____ _ _ ');
|
||||
console.log(' | |_|___ ___| |_ ___ _ _ ');
|
||||
console.log(' | | | | |_ -|_ -| \'_| -_| | |');
|
||||
@ -34,7 +35,7 @@ function greet(config: Config) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
greet();
|
||||
|
||||
// initialize app
|
||||
config = await init();
|
||||
|
||||
greet(config);
|
||||
|
||||
if (config.port == null || Number.isNaN(config.port)) {
|
||||
bootLogger.error('The port is not configured. Please configure port.', null, true);
|
||||
process.exit(1);
|
||||
|
@ -115,7 +115,6 @@ export default Vue.extend({
|
||||
connection: null,
|
||||
meta: null,
|
||||
instances: [],
|
||||
clock: null,
|
||||
faDatabase
|
||||
};
|
||||
},
|
||||
@ -124,7 +123,6 @@ export default Vue.extend({
|
||||
this.connection = this.$root.stream.useSharedConnection('serverStats');
|
||||
|
||||
this.updateStats();
|
||||
this.clock = setInterval(this.updateStats, 3000);
|
||||
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.meta = meta;
|
||||
@ -145,7 +143,6 @@ export default Vue.extend({
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -81,6 +81,7 @@
|
||||
</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="proxyRemoteFiles">{{ $t('proxy-remote-files') }}<template #desc>{{ $t('proxy-remote-files-desc') }}</template></ui-switch>
|
||||
</section>
|
||||
<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>
|
||||
@ -275,6 +276,7 @@ export default Vue.extend({
|
||||
description: null,
|
||||
languages: null,
|
||||
cacheRemoteFiles: false,
|
||||
proxyRemoteFiles: false,
|
||||
localDriveCapacityMb: null,
|
||||
remoteDriveCapacityMb: null,
|
||||
maxNoteTextLength: null,
|
||||
@ -339,6 +341,7 @@ export default Vue.extend({
|
||||
this.description = meta.description;
|
||||
this.languages = meta.langs.join(' ');
|
||||
this.cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
this.proxyRemoteFiles = meta.proxyRemoteFiles;
|
||||
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||
@ -463,6 +466,7 @@ export default Vue.extend({
|
||||
description: this.description,
|
||||
langs: this.languages ? this.languages.split(' ') : [],
|
||||
cacheRemoteFiles: this.cacheRemoteFiles,
|
||||
proxyRemoteFiles: this.proxyRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||
maxNoteTextLength: parseInt(this.maxNoteTextLength, 10),
|
||||
|
@ -48,14 +48,15 @@
|
||||
</ui-select>
|
||||
</ui-horizon-group>
|
||||
<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>
|
||||
<template v-if="domain === 'deliver'">
|
||||
<span>{{ job.data.to }}</span>
|
||||
</template>
|
||||
<template v-if="domain === 'inbox'">
|
||||
<span>{{ job.activity.id }}</span>
|
||||
<span>{{ job.data.activity.id }}</span>
|
||||
</template>
|
||||
<span>{{ `(${job.attempts}/${job.maxAttempts}, ${Math.floor((jobsFetched - job.timestamp) / 1000 / 60)}min)` }}</span>
|
||||
</div>
|
||||
</sequential-entrance>
|
||||
<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,
|
||||
jobs: [],
|
||||
jobsLimit: 50,
|
||||
jobsFetched: Date.now(),
|
||||
domain: 'deliver',
|
||||
state: 'delayed',
|
||||
faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud
|
||||
@ -140,6 +142,7 @@ export default Vue.extend({
|
||||
state: this.state,
|
||||
limit: this.jobsLimit
|
||||
}).then(jobs => {
|
||||
this.jobsFetched = Date.now(),
|
||||
this.jobs = jobs;
|
||||
});
|
||||
},
|
||||
@ -149,7 +152,8 @@ export default Vue.extend({
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.xvvuvgsv
|
||||
> b
|
||||
margin-right 16px
|
||||
margin-left -6px
|
||||
> b, span
|
||||
margin 0 6px
|
||||
|
||||
</style>
|
||||
|
@ -72,13 +72,17 @@
|
||||
//#region Fetch locale data
|
||||
const cachedLocale = localStorage.getItem('locale');
|
||||
const localeKey = localStorage.getItem('localeKey');
|
||||
let localeData = null;
|
||||
|
||||
if (cachedLocale == null || localeKey != `${ver}.${lang}`) {
|
||||
const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`)
|
||||
.then(response => response.json());
|
||||
localeData = locale;
|
||||
|
||||
localStorage.setItem('locale', JSON.stringify(locale));
|
||||
localStorage.setItem('localeKey', `${ver}.${lang}`);
|
||||
} else {
|
||||
localeData = JSON.parse(cachedLocale);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@ -99,8 +103,7 @@
|
||||
// If mobile, insert the viewport meta tag
|
||||
if (isMobile) {
|
||||
const viewport = document.getElementsByName("viewport").item(0);
|
||||
viewport.setAttribute('content',
|
||||
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
|
||||
viewport.content = `${viewport.content},minimum-scale=1,maximum-scale=1,user-scalable=no`;
|
||||
head.appendChild(viewport);
|
||||
}
|
||||
|
||||
@ -113,9 +116,9 @@
|
||||
// Note: 'async' make it possible to load the script asyncly.
|
||||
// 'defer' make it possible to run the script when the dom loaded.
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', `/assets/${app}.${ver}.js`);
|
||||
script.setAttribute('async', 'true');
|
||||
script.setAttribute('defer', 'true');
|
||||
script.src = `/assets/${app}.${ver}.js`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
head.appendChild(script);
|
||||
|
||||
// 3秒経ってもスクリプトがロードされない場合はバージョンが古くて
|
||||
@ -138,10 +141,10 @@
|
||||
localStorage.setItem('v', meta.version);
|
||||
|
||||
alert(
|
||||
'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
|
||||
'\n\n' +
|
||||
'New version of Misskey available. The page will be reloaded.');
|
||||
|
||||
localeData.common._settings["update-available"] +
|
||||
'\n' +
|
||||
localeData.common._settings["update-available-desc"]
|
||||
);
|
||||
refresh();
|
||||
}
|
||||
}, 3000);
|
||||
|
@ -42,7 +42,7 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
previewable(file) {
|
||||
return file.type.startsWith('video') || file.type.startsWith('image');
|
||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && file.thumbnailUrl;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
@ -6,9 +6,13 @@
|
||||
<header>{{ $t('mute') }}</header>
|
||||
<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
|
||||
<div class="users" v-if="mute.length != 0">
|
||||
<div v-for="user in mute" :key="user.id">
|
||||
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
|
||||
<div class="user" v-for="user in mute" :key="user.id">
|
||||
<x-user :user="user"/>
|
||||
<span @click="unmute(user)">
|
||||
<fa icon="times"/>
|
||||
</span>
|
||||
</div>
|
||||
<ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -16,9 +20,13 @@
|
||||
<header>{{ $t('block') }}</header>
|
||||
<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
|
||||
<div class="users" v-if="block.length != 0">
|
||||
<div v-for="user in block" :key="user.id">
|
||||
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
|
||||
<div class="user" v-for="user in block" :key="user.id">
|
||||
<x-user :user="user"/>
|
||||
<span @click="unblock(user)">
|
||||
<fa icon="times"/>
|
||||
</span>
|
||||
</div>
|
||||
<ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -35,16 +43,25 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import XUser from './mute-and-block.user.vue';
|
||||
|
||||
const fetchLimit = 30;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/mute-and-block.vue'),
|
||||
|
||||
components: {
|
||||
XUser
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
muteFetching: true,
|
||||
blockFetching: true,
|
||||
mute: [],
|
||||
block: [],
|
||||
muteCursor: undefined,
|
||||
blockCursor: undefined,
|
||||
mutedWords: ''
|
||||
};
|
||||
},
|
||||
@ -59,21 +76,106 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n');
|
||||
|
||||
this.$root.api('mute/list').then(mute => {
|
||||
this.mute = mute.map(x => x.mutee);
|
||||
this.muteFetching = false;
|
||||
});
|
||||
|
||||
this.$root.api('blocking/list').then(blocking => {
|
||||
this.block = blocking.map(x => x.blockee);
|
||||
this.blockFetching = false;
|
||||
});
|
||||
this.updateMute();
|
||||
this.updateBlock();
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
|
@ -76,13 +76,15 @@ export default Vue.extend({
|
||||
this.$root.api('ap/show', {
|
||||
uri: acct
|
||||
}).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({
|
||||
type: 'error',
|
||||
text: 'acct is not an user'
|
||||
text: 'Not supported'
|
||||
});
|
||||
} else {
|
||||
this.user = res.object;
|
||||
}
|
||||
}).catch((e: any) => {
|
||||
this.$root.dialog({
|
||||
|
@ -43,6 +43,8 @@ export default function load() {
|
||||
|
||||
if (config.autoAdmin == null) config.autoAdmin = false;
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,8 @@ export type Source = {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
mediaProxy?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"copyright": "Copyright (c) 2014-2019 syuilo"
|
||||
"copyright": "Copyright (c) 2014-2020 syuilo"
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.pass,
|
||||
prefix: config.redis.prefix,
|
||||
prefix: `${config.redis.prefix}:query:`,
|
||||
db: config.redis.db || 0
|
||||
}
|
||||
} : false,
|
||||
|
@ -162,6 +162,7 @@ export class ASEvaluator {
|
||||
multiply: (a: number, b: number) => a * b,
|
||||
divide: (a: number, b: number) => a / b,
|
||||
mod: (a: number, b: number) => a % b,
|
||||
round: (a: number) => Math.round(a),
|
||||
strLen: (a: string) => a.length,
|
||||
strPick: (a: string, b: number) => a[b - 1],
|
||||
strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
faExchangeAlt,
|
||||
faRecycle,
|
||||
faIndent,
|
||||
faCalculator,
|
||||
} from '@fortawesome/free-solid-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, },
|
||||
divide: { 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, },
|
||||
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
|
||||
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },
|
||||
|
@ -1,15 +1,15 @@
|
||||
import * as fs from 'fs';
|
||||
import fileType = require('file-type');
|
||||
import checkSvg from '../misc/check-svg';
|
||||
const FileType = require('file-type');
|
||||
|
||||
export async function detectMine(path: string) {
|
||||
return new Promise<[string, string | null]>((res, rej) => {
|
||||
const readable = fs.createReadStream(path);
|
||||
readable
|
||||
.on('error', rej)
|
||||
.once('data', (buffer: Buffer) => {
|
||||
.once('data', async (buffer: Buffer) => {
|
||||
readable.destroy();
|
||||
const type = fileType(buffer);
|
||||
const type = await FileType.fromBuffer(buffer);
|
||||
if (type) {
|
||||
if (type.mime == 'application/xml' && checkSvg(path)) {
|
||||
res(['image/svg+xml', 'svg']);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as request from 'request';
|
||||
import config from '../config';
|
||||
import chalk from 'chalk';
|
||||
import * as chalk from 'chalk';
|
||||
import Logger from '../services/logger';
|
||||
|
||||
export async function downloadUrl(url: string, path: string) {
|
||||
|
@ -64,6 +64,11 @@ export class MessagingMessage {
|
||||
})
|
||||
public isRead: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}'
|
||||
|
@ -115,6 +115,11 @@ export class Meta {
|
||||
})
|
||||
public cacheRemoteFiles: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public proxyRemoteFiles: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true
|
||||
|
@ -6,6 +6,10 @@ import { toPuny } from '../../misc/convert-host';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
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>;
|
||||
|
||||
@ -21,8 +25,27 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
);
|
||||
}
|
||||
|
||||
public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
|
||||
return thumbnail ? (file.thumbnailUrl || file.webpublicUrl || null) : (file.webpublicUrl || file.url);
|
||||
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
|
||||
// リモートかつメディアプロキシ
|
||||
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> {
|
||||
@ -82,6 +105,8 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
|
||||
const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
const meta = await fetchMeta();
|
||||
|
||||
return await awaitAll({
|
||||
id: file.id,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
@ -91,8 +116,8 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
size: file.size,
|
||||
isSensitive: file.isSensitive,
|
||||
properties: file.properties,
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
thumbnailUrl: this.getPublicUrl(file, true),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
|
||||
thumbnailUrl: this.getPublicUrl(file, true, meta),
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
|
||||
detail: true
|
||||
|
@ -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() {
|
||||
const reaction = await NoteReactions.findOne({
|
||||
userId: meId!,
|
||||
@ -148,8 +173,6 @@ export class NoteRepository extends Repository<Note> {
|
||||
text = `【${note.name}】\n${(note.text || '').trim()}\n${note.uri}`;
|
||||
}
|
||||
|
||||
const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
|
||||
|
||||
const packed = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
@ -166,10 +189,7 @@ export class NoteRepository extends Repository<Note> {
|
||||
repliesCount: note.repliesCount,
|
||||
reactions: note.reactions,
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
emojis: reactionEmojis.length > 0 ? Emojis.find({
|
||||
name: In(reactionEmojis),
|
||||
host: host
|
||||
}) : [],
|
||||
emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)),
|
||||
fileIds: note.fileIds,
|
||||
files: DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
|
15
src/queue/get-job-info.ts
Normal file
15
src/queue/get-job-info.ts
Normal 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}`;
|
||||
}
|
@ -11,6 +11,7 @@ import processDb from './processors/db';
|
||||
import procesObjectStorage from './processors/object-storage';
|
||||
import { queueLogger } from './logger';
|
||||
import { DriveFile } from '../models/entities/drive-file';
|
||||
import { getJobInfo } from './get-job-info';
|
||||
|
||||
function initializeQueue(name: string) {
|
||||
return new Queue(name, {
|
||||
@ -44,19 +45,19 @@ const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||
|
||||
deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`, { job, e: renderError(err) }))
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} 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}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.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
|
||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
|
||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.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('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
|
||||
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
|
||||
|
@ -30,7 +30,12 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
|
||||
const csv = await downloadTextFile(file.url);
|
||||
|
||||
let linenum = 0;
|
||||
|
||||
for (const line of csv.trim().split('\n')) {
|
||||
linenum++;
|
||||
|
||||
try {
|
||||
const listName = line.split(',')[0].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;
|
||||
|
||||
pushUserToUserList(target, list);
|
||||
} catch (e) {
|
||||
logger.warn(`Error in line:${linenum} ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.succ('Imported');
|
||||
|
@ -5,6 +5,8 @@ import Logger from '../../services/logger';
|
||||
import { Instances } from '../../models';
|
||||
import { instanceChart } from '../../services/chart';
|
||||
import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
@ -13,6 +15,23 @@ let latest: string | null = null;
|
||||
export default async (job: Bull.Job) => {
|
||||
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 {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
logger.debug(`delivering ${latest}`);
|
||||
@ -48,8 +67,6 @@ export default async (job: Bull.Job) => {
|
||||
});
|
||||
|
||||
if (res != null && res.hasOwnProperty('statusCode')) {
|
||||
logger.warn(`deliver failed: ${res.statusCode} ${res.statusMessage} to=${job.data.to}`);
|
||||
|
||||
// 4xx
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||
@ -61,7 +78,6 @@ export default async (job: Bull.Job) => {
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
logger.warn(`deliver failed: ${res} to=${job.data.to}`);
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
|
131
src/remote/activitypub/deliver-manager.ts
Normal file
131
src/remote/activitypub/deliver-manager.ts
Normal 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
|
@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let object;
|
||||
|
||||
try {
|
||||
object = await resolver.resolve(activity.object);
|
||||
} catch (e) {
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import announceNote from './note';
|
||||
import { IAnnounce, validPost, getApId } from '../../type';
|
||||
import { IAnnounce, getApId } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
const logger = apLogger;
|
||||
@ -13,18 +13,7 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let object;
|
||||
const targetUri = getApId(activity.object);
|
||||
|
||||
try {
|
||||
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}`);
|
||||
}
|
||||
announceNote(resolver, actor, activity, targetUri);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Resolver from '../../resolver';
|
||||
import post from '../../../../services/note/create';
|
||||
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 { resolvePerson } from '../../models/person';
|
||||
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);
|
||||
|
||||
// アナウンサーが凍結されていたらスキップ
|
||||
@ -38,14 +38,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
||||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await resolveNote(note);
|
||||
renote = await resolveNote(targetUri);
|
||||
} catch (e) {
|
||||
// 対象が4xxならスキップ
|
||||
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;
|
||||
}
|
||||
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
|
||||
logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let object;
|
||||
|
||||
try {
|
||||
object = await resolver.resolve(activity.object);
|
||||
} catch (e) {
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
if (validPost.includes(object.type)) {
|
||||
createNote(resolver, actor, object);
|
||||
|
@ -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 create from './create';
|
||||
import performDeleteActivity from './delete';
|
||||
import performUpdateActivity from './update';
|
||||
import { performReadActivity } from './read';
|
||||
import follow from './follow';
|
||||
import undo from './undo';
|
||||
import like from './like';
|
||||
@ -41,6 +42,8 @@ async function performOneActivity(actor: IRemoteUser, activity: IObject): Promis
|
||||
await performDeleteActivity(actor, activity);
|
||||
} else if (isUpdate(activity)) {
|
||||
await performUpdateActivity(actor, activity);
|
||||
} else if (isRead(activity)) {
|
||||
await performReadActivity(actor, activity);
|
||||
} else if (isFollow(activity)) {
|
||||
await follow(actor, activity);
|
||||
} else if (isAccept(activity)) {
|
||||
|
27
src/remote/activitypub/kernel/read.ts
Normal file
27
src/remote/activitypub/kernel/read.ts
Normal 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})`;
|
||||
};
|
@ -13,14 +13,10 @@ export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let object;
|
||||
|
||||
try {
|
||||
object = await resolver.resolve(activity.object);
|
||||
} catch (e) {
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
|
@ -20,14 +20,10 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let object;
|
||||
|
||||
try {
|
||||
object = await resolver.resolve(activity.object);
|
||||
} catch (e) {
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
switch (object.type) {
|
||||
case 'Follow':
|
||||
|
@ -162,16 +162,42 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
|
||||
// 引用
|
||||
let quote: Note | undefined | null;
|
||||
|
||||
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
|
||||
quote = await resolveNote(note._misskey_quote).catch(e => {
|
||||
// 4xxの場合は引用してないことにする
|
||||
if (e.statusCode >= 400 && e.statusCode < 500) {
|
||||
logger.warn(`Ignored quote target ${note.inReplyTo} - ${e.statusCode} `);
|
||||
return null;
|
||||
if (note._misskey_quote || note.quoteUrl) {
|
||||
const tryResolveNote = async (uri: string): Promise<{
|
||||
status: 'ok';
|
||||
res: Note | 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;
|
||||
@ -226,7 +252,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
|
||||
|
||||
if (note._misskey_talk && visibility === 'specified') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import config from '../../../config';
|
||||
import Resolver from '../resolver';
|
||||
import { resolveImage } from './image';
|
||||
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type';
|
||||
import { DriveFile } from '../../../models/entities/drive-file';
|
||||
import { fromHtml } from '../../../mfm/fromHtml';
|
||||
import { resolveNote, extractEmojis } from './note';
|
||||
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);
|
||||
|
||||
//#region アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = (await Promise.all<DriveFile | null>([
|
||||
const [avatar, banner] = await Promise.all([
|
||||
person.icon,
|
||||
person.image
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(user!, img).catch(() => null)
|
||||
)));
|
||||
));
|
||||
|
||||
const avatarId = avatar ? avatar.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 avatarColor = avatar && avatar.properties.avgColor ? avatar.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}`);
|
||||
|
||||
// アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = (await Promise.all<DriveFile | null>([
|
||||
const [avatar, banner] = await Promise.all([
|
||||
person.icon,
|
||||
person.image
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(exist, img).catch(() => null)
|
||||
)));
|
||||
));
|
||||
|
||||
// カスタム絵文字取得
|
||||
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) {
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar);
|
||||
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
|
||||
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
||||
}
|
||||
|
||||
|
@ -159,6 +159,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||
content,
|
||||
_misskey_content: text,
|
||||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
|
@ -6,7 +6,7 @@
|
||||
* @param last URL of last page (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 = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
|
9
src/remote/activitypub/renderer/read.ts
Normal file
9
src/remote/activitypub/renderer/read.ts
Normal 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
|
||||
});
|
@ -6,15 +6,10 @@ import * as cache from 'lookup-dns-cache';
|
||||
import config from '../../config';
|
||||
import { ILocalUser } from '../../models/entities/user';
|
||||
import { publishApLogStream } from '../../services/stream';
|
||||
import { apLogger } from './logger';
|
||||
import { UserKeypairs, Instances } from '../../models';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
import { UserKeypairs } from '../../models';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import * as httpsProxyAgent from 'https-proxy-agent';
|
||||
|
||||
export const logger = apLogger.createSubLogger('deliver');
|
||||
|
||||
const agent = config.proxy
|
||||
? new httpsProxyAgent(config.proxy)
|
||||
: new https.Agent({
|
||||
@ -24,28 +19,7 @@ const agent = config.proxy
|
||||
export default async (user: ILocalUser, url: string, object: any) => {
|
||||
const timeout = 10 * 1000;
|
||||
|
||||
const { protocol, host, 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 { protocol, hostname, port, pathname, search } = new URL(url);
|
||||
|
||||
const data = JSON.stringify(object);
|
||||
|
||||
@ -73,10 +47,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
|
||||
}
|
||||
}, res => {
|
||||
if (res.statusCode! >= 400) {
|
||||
logger.warn(`${url} --> ${res.statusCode}`);
|
||||
reject(res);
|
||||
} else {
|
||||
logger.succ(`${url} --> ${res.statusCode}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
@ -88,11 +60,6 @@ export default async (user: ILocalUser, url: string, object: any) => {
|
||||
headers: ['date', 'host', 'digest']
|
||||
});
|
||||
|
||||
// Signature: Signature ... => Signature: ...
|
||||
let sig = req.getHeader('Signature')!.toString();
|
||||
sig = sig.replace(/^Signature /, '');
|
||||
req.setHeader('Signature', sig);
|
||||
|
||||
req.on('timeout', () => req.abort());
|
||||
|
||||
req.on('error', e => {
|
||||
|
@ -51,6 +51,13 @@ export default class Resolver {
|
||||
Accept: 'application/activity+json, application/ld+json'
|
||||
},
|
||||
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 || (
|
||||
|
@ -75,6 +75,7 @@ export interface INote extends IObject {
|
||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||
_misskey_content?: string;
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
_misskey_talk: boolean;
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ export interface IQuestion extends IObject {
|
||||
type: 'Note' | 'Question';
|
||||
_misskey_content?: string;
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
oneOf?: IQuestionChoice[];
|
||||
anyOf?: IQuestionChoice[];
|
||||
endTime?: Date;
|
||||
@ -140,6 +142,10 @@ export interface IUpdate extends IActivity {
|
||||
type: 'Update';
|
||||
}
|
||||
|
||||
export interface IRead extends IActivity {
|
||||
type: 'Read';
|
||||
}
|
||||
|
||||
export interface IUndo extends IActivity {
|
||||
type: 'Undo';
|
||||
}
|
||||
@ -180,6 +186,7 @@ export interface IBlock extends IActivity {
|
||||
export const isCreate = (object: IObject): object is ICreate => object.type === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => object.type === 'Delete';
|
||||
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 isFollow = (object: IObject): object is IFollow => object.type === 'Follow';
|
||||
export const isAccept = (object: IObject): object is IAccept => object.type === 'Accept';
|
||||
|
@ -2,7 +2,7 @@ import webFinger from './webfinger';
|
||||
import config from '../config';
|
||||
import { createPerson, updatePerson } from './activitypub/models/person';
|
||||
import { remoteLogger } from './logger';
|
||||
import chalk from 'chalk';
|
||||
import * as chalk from 'chalk';
|
||||
import { User, IRemoteUser } from '../models/entities/user';
|
||||
import { Users } from '../models';
|
||||
import { toPuny } from '../misc/convert-host';
|
||||
|
@ -26,8 +26,6 @@ const router = new Router();
|
||||
function inbox(ctx: Router.RouterContext) {
|
||||
let signature;
|
||||
|
||||
ctx.req.headers.authorization = `Signature ${ctx.req.headers.signature}`;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
|
||||
} catch (e) {
|
||||
@ -167,7 +165,8 @@ router.get('/users/:user', async (ctx, next) => {
|
||||
|
||||
const user = await Users.findOne({
|
||||
id: userId,
|
||||
host: null
|
||||
host: null,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
await userInfo(ctx, user);
|
||||
@ -178,7 +177,8 @@ router.get('/@:user', async (ctx, next) => {
|
||||
|
||||
const user = await Users.findOne({
|
||||
usernameLower: ctx.params.user.toLowerCase(),
|
||||
host: null
|
||||
host: null,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
await userInfo(ctx, user);
|
||||
|
@ -5,7 +5,7 @@ import authenticate from './authenticate';
|
||||
import call from './call';
|
||||
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 reply = (x?: any, y?: ApiError) => {
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
|
||||
import { publishMessagingStream } 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 { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
|
||||
import { In } from 'typeorm';
|
||||
import { IdentifiableError } from '../../../misc/identifiable-error';
|
||||
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
|
||||
@ -101,3 +106,17 @@ export async function readGroupMessagingMessage(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { Signins } from '../../../models';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
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) {
|
||||
//#region Cookie
|
||||
const expires = 1000 * 60 * 60 * 24 * 365; // One Year
|
||||
|
@ -1,6 +1,6 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { deliverQueue, inboxQueue } from '../../../../../queue';
|
||||
import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '../../../../../queue';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@ -10,11 +10,11 @@ export const meta = {
|
||||
|
||||
params: {
|
||||
domain: {
|
||||
validator: $.str,
|
||||
validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']),
|
||||
},
|
||||
|
||||
state: {
|
||||
validator: $.str,
|
||||
validator: $.str.or(['active', 'waiting', 'delayed']),
|
||||
},
|
||||
|
||||
limit: {
|
||||
@ -28,13 +28,22 @@ export default define(meta, async (ps) => {
|
||||
const queue =
|
||||
ps.domain === 'deliver' ? deliverQueue :
|
||||
ps.domain === 'inbox' ? inboxQueue :
|
||||
ps.domain === 'db' ? dbQueue :
|
||||
ps.domain === 'objectStorage' ? objectStorageQueue :
|
||||
null as never;
|
||||
|
||||
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,
|
||||
data: job.data,
|
||||
data,
|
||||
attempts: job.attemptsMade,
|
||||
}));
|
||||
maxAttempts: job.opts ? job.opts.attempts : 0,
|
||||
timestamp: job.timestamp,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import $ from 'cafy';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import deleteFollowing from '../../../../services/following/delete';
|
||||
import { Users, Followings } from '../../../../models';
|
||||
import { Users, Followings, Notifications } from '../../../../models';
|
||||
import { User } from '../../../../models/entities/user';
|
||||
import { insertModerationLog } from '../../../../services/insert-moderation-log';
|
||||
import { doPostSuspend } from '../../../../services/suspend-user';
|
||||
@ -55,6 +55,7 @@ export default define(meta, async (ps, me) => {
|
||||
(async () => {
|
||||
await doPostSuspend(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);
|
||||
}
|
||||
}
|
||||
|
||||
async function readAllNotify(notifier: User) {
|
||||
await Notifications.update({
|
||||
notifierId: notifier.id,
|
||||
isRead: false,
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
}
|
||||
|
@ -151,6 +151,13 @@ export const meta = {
|
||||
}
|
||||
},
|
||||
|
||||
proxyRemoteFiles: {
|
||||
validator: $.optional.bool,
|
||||
desc: {
|
||||
'ja-JP': 'ローカルにないリモートのファイルをプロキシするか否か'
|
||||
}
|
||||
},
|
||||
|
||||
enableRecaptcha: {
|
||||
validator: $.optional.bool,
|
||||
desc: {
|
||||
@ -478,6 +485,10 @@ export default define(meta, async (ps, me) => {
|
||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||
}
|
||||
|
||||
if (ps.proxyRemoteFiles !== undefined) {
|
||||
set.proxyRemoteFiles = ps.proxyRemoteFiles;
|
||||
}
|
||||
|
||||
if (ps.enableRecaptcha !== undefined) {
|
||||
set.enableRecaptcha = ps.enableRecaptcha;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
|
||||
import { readNotification } from '../../common/read-notification';
|
||||
import define from '../../define';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notifications, Followings, Mutings } from '../../../../models';
|
||||
import { Notifications, Followings, Mutings, Users } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -72,6 +72,10 @@ export default define(meta, async (ps, user) => {
|
||||
.select('muting.muteeId')
|
||||
.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)
|
||||
.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
|
||||
.leftJoinAndSelect('notification.notifier', 'notifier');
|
||||
@ -79,6 +83,8 @@ export default define(meta, async (ps, user) => {
|
||||
query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
query.setParameters(mutingQuery.getParameters());
|
||||
|
||||
query.andWhere(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`);
|
||||
|
||||
if (ps.following) {
|
||||
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
|
@ -3,10 +3,10 @@ import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
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 { Brackets } from 'typeorm';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -114,6 +114,11 @@ export default define(meta, async (ps, user) => {
|
||||
// Mark all as read
|
||||
if (ps.markAsRead) {
|
||||
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, {
|
||||
|
@ -143,6 +143,7 @@ export default define(meta, async (ps, me) => {
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
proxyRemoteFiles: instance.proxyRemoteFiles,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
swPublickey: instance.swPublicKey,
|
||||
|
@ -66,7 +66,7 @@ export default define(meta, async (ps, user) => {
|
||||
}))
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (user) generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
|
||||
const notes = await query.take(ps.limit!).getMany();
|
||||
|
@ -95,7 +95,7 @@ export default define(meta, async (ps, user) => {
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (user) generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
@ -70,7 +70,7 @@ export default define(meta, async (ps, user) => {
|
||||
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (user) generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
|
||||
const renotes = await query.take(ps.limit!).getMany();
|
||||
|
@ -61,7 +61,7 @@ export default define(meta, async (ps, user) => {
|
||||
.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (user) generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
@ -95,7 +95,7 @@ export default define(meta, async (ps, me) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (me) generateVisibilityQuery(query, me);
|
||||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMuteQuery(query, me);
|
||||
|
||||
if (ps.tag) {
|
||||
|
@ -133,7 +133,7 @@ export default define(meta, async (ps, me) => {
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (me) generateVisibilityQuery(query, me);
|
||||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMuteQuery(query, me, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
@ -66,13 +66,18 @@ export const meta = {
|
||||
export default define(meta, async (ps, me) => {
|
||||
let user;
|
||||
|
||||
const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
|
||||
|
||||
if (ps.userIds) {
|
||||
if (ps.userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const users = await Users.find({
|
||||
const users = await Users.find(isAdminOrModerator ? {
|
||||
id: In(ps.userIds)
|
||||
} : {
|
||||
id: In(ps.userIds),
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
if (user == null || (!isAdminOrModerator && user.isSuspended)) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { ensure } from '../../../prelude/ensure';
|
||||
import { verifyLogin, hash } from '../2fa';
|
||||
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-Credentials', 'true');
|
||||
|
||||
|
@ -15,8 +15,8 @@ import { UserProfile } from '../../../models/entities/user-profile';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { UsedUsername } from '../../../models/entities/used-username';
|
||||
|
||||
export default async (ctx: Koa.BaseContext) => {
|
||||
const body = ctx.request.body as any;
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const body = ctx.request.body;
|
||||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
|
@ -12,11 +12,11 @@ import { Users, UserProfiles } from '../../../models';
|
||||
import { ILocalUser } from '../../../models/entities/user';
|
||||
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];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext) {
|
||||
function compareOrigin(ctx: Koa.Context) {
|
||||
function normalizeUrl(url: string) {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
@ -12,11 +12,11 @@ import { Users, UserProfiles } from '../../../models';
|
||||
import { ILocalUser } from '../../../models/entities/user';
|
||||
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];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext) {
|
||||
function compareOrigin(ctx: Koa.Context) {
|
||||
function normalizeUrl(url: string) {
|
||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ import { Users, UserProfiles } from '../../../models';
|
||||
import { ILocalUser } from '../../../models/entities/user';
|
||||
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];
|
||||
}
|
||||
|
||||
function compareOrigin(ctx: Koa.BaseContext) {
|
||||
function compareOrigin(ctx: Koa.Context) {
|
||||
function normalizeUrl(url: string) {
|
||||
return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
|
||||
}
|
||||
|
@ -25,17 +25,8 @@ export default class extends Channel {
|
||||
@autobind
|
||||
private async onNote(note: PackedNote) {
|
||||
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
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user, {
|
||||
@ -48,7 +39,6 @@ export default class extends Channel {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (shouldMuteThisNote(note, this.muting)) return;
|
||||
|
@ -1,7 +1,8 @@
|
||||
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 { UserGroupJoinings } from '../../../../models';
|
||||
import { UserGroupJoinings, Users, MessagingMessages } from '../../../../models';
|
||||
import { User, ILocalUser, IRemoteUser } from '../../../../models/entities/user';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'messaging';
|
||||
@ -9,11 +10,13 @@ export default class extends Channel {
|
||||
public static requireCredential = true;
|
||||
|
||||
private otherpartyId: string | null;
|
||||
private otherparty?: User;
|
||||
private groupId: string | null;
|
||||
|
||||
@autobind
|
||||
public async init(params: any) {
|
||||
this.otherpartyId = params.otherparty as string;
|
||||
this.otherparty = await Users.findOne({ id: this.otherpartyId });
|
||||
this.groupId = params.group as string;
|
||||
|
||||
// Check joining
|
||||
@ -44,6 +47,13 @@ export default class extends Channel {
|
||||
case 'read':
|
||||
if (this.otherpartyId) {
|
||||
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) {
|
||||
readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ module.exports = (server: http.Server) => {
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
|
||||
subscriber.subscribe('misskey');
|
||||
subscriber.subscribe(config.host);
|
||||
|
||||
ev = new EventEmitter();
|
||||
|
||||
|
@ -12,19 +12,14 @@ import sendDriveFile from './send-drive-file';
|
||||
const app = new Koa();
|
||||
app.use(cors());
|
||||
|
||||
app.use(async (ctx, next) => {
|
||||
// Cache 365days
|
||||
ctx.set('Cache-Control', 'max-age=31536000, immutable');
|
||||
await next();
|
||||
});
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
router.get('/app-default.jpg', ctx => {
|
||||
const file = fs.createReadStream(`${__dirname}/assets/dummy.png`);
|
||||
ctx.set('Content-Type', 'image/jpeg');
|
||||
ctx.body = file;
|
||||
ctx.set('Content-Type', 'image/jpeg');
|
||||
ctx.set('Cache-Control', 'max-age=31536000, immutable');
|
||||
});
|
||||
|
||||
router.get('/:key', sendDriveFile);
|
||||
|
@ -1,19 +1,26 @@
|
||||
import * as Koa from 'koa';
|
||||
import * as send from 'koa-send';
|
||||
import * as rename from 'rename';
|
||||
import * as tmp from 'tmp';
|
||||
import * as fs from 'fs';
|
||||
import { serverLogger } from '..';
|
||||
import { contentDisposition } from '../../misc/content-disposition';
|
||||
import { DriveFiles } from '../../models';
|
||||
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 commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): void => {
|
||||
const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
|
||||
serverLogger.error(e);
|
||||
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;
|
||||
|
||||
// Fetch drive file
|
||||
@ -25,32 +32,88 @@ export default async function(ctx: Koa.BaseContext) {
|
||||
|
||||
if (file == null) {
|
||||
ctx.status = 404;
|
||||
ctx.set('Cache-Control', 'max-age=86400');
|
||||
await send(ctx as any, '/dummy.png', { root: assets });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.storedInternal) {
|
||||
ctx.status = 204;
|
||||
return;
|
||||
}
|
||||
|
||||
const isThumbnail = file.thumbnailAccessKey === key;
|
||||
const isWebpublic = file.webpublicAccessKey === key;
|
||||
|
||||
if (isThumbnail) {
|
||||
ctx.set('Content-Type', 'image/jpeg');
|
||||
ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`));
|
||||
ctx.body = InternalStorage.read(key);
|
||||
} else if (isWebpublic) {
|
||||
ctx.set('Content-Type', file.type === 'image/apng' ? 'image/png' : file.type);
|
||||
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}`));
|
||||
if (!file.storedInternal) {
|
||||
if (file.isLink && file.uri) { // 期限切れリモートファイル
|
||||
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
tmp.file((e, path, fd, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
});
|
||||
});
|
||||
|
||||
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!);
|
||||
readable.on('error', commonReadableHandlerGenerator(ctx));
|
||||
ctx.set('Content-Type', file.type);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { createTemp } from '../../misc/create-temp';
|
||||
import { downloadUrl } from '../../misc/donwload-url';
|
||||
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;
|
||||
|
||||
// Create temp file
|
||||
|
@ -101,7 +101,8 @@ const getFeed = async (acct: string) => {
|
||||
const { username, host } = parseAcct(acct);
|
||||
const user = await Users.findOne({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host
|
||||
host,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
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 user = await Users.findOne({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host
|
||||
host,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
if (user != null) {
|
||||
@ -170,6 +172,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||
ctx.set('Cache-Control', 'public, max-age=30');
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
||||
await next();
|
||||
}
|
||||
});
|
||||
@ -177,7 +180,8 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||
router.get('/users/:user', async ctx => {
|
||||
const user = await Users.findOne({
|
||||
id: ctx.params.user,
|
||||
host: null
|
||||
host: null,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
|
@ -2,7 +2,7 @@ import * as Koa from 'koa';
|
||||
import * as manifest from '../../client/assets/manifest.json';
|
||||
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 instance = await fetchMeta(true);
|
||||
|
@ -8,7 +8,7 @@ import { query } from '../../prelude/url';
|
||||
|
||||
const logger = new Logger('url-preview');
|
||||
|
||||
module.exports = async (ctx: Koa.BaseContext) => {
|
||||
module.exports = async (ctx: Koa.Context) => {
|
||||
const meta = await fetchMeta();
|
||||
|
||||
logger.info(meta.summalyProxy
|
||||
|
@ -49,7 +49,8 @@ router.get('/.well-known/nodeinfo', async ctx => {
|
||||
router.get(webFingerPath, async ctx => {
|
||||
const fromId = (id: User['id']): Record<string, any> => ({
|
||||
id,
|
||||
host: null
|
||||
host: null,
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
const generateQuery = (resource: string) =>
|
||||
@ -63,7 +64,8 @@ router.get(webFingerPath, async ctx => {
|
||||
const fromAcct = (acct: Acct): Record<string, any> | number =>
|
||||
!acct.host || acct.host === config.host.toLowerCase() ? {
|
||||
usernameLower: acct.username,
|
||||
host: null
|
||||
host: null,
|
||||
isSuspended: false
|
||||
} : 422;
|
||||
|
||||
if (typeof ctx.query.resource !== 'string') {
|
||||
|
@ -10,7 +10,7 @@ import { deleteFile } from './delete-file';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { GenerateVideoThumbnail } from './generate-video-thumbnail';
|
||||
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 { detectMine } from '../../misc/detect-mine';
|
||||
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);
|
||||
} else if (['image/png'].includes(type)) {
|
||||
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 {
|
||||
logger.info(`web image not created (not an image)`);
|
||||
logger.debug(`web image not created (not an required image)`);
|
||||
}
|
||||
} catch (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);
|
||||
} else if (['image/png'].includes(type)) {
|
||||
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/')) {
|
||||
try {
|
||||
thumbnail = await GenerateVideoThumbnail(path);
|
||||
} catch (e) {
|
||||
logger.error(`GenerateVideoThumbnail failed: ${e}`);
|
||||
logger.warn(`GenerateVideoThumbnail failed: ${e}`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`thumbnail not created (not an required file)`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`thumbnail not created (an error occured)`, e);
|
||||
@ -361,7 +355,7 @@ export default async function(
|
||||
|
||||
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) {
|
||||
const img = sharp(path);
|
||||
@ -384,8 +378,9 @@ export default async function(
|
||||
logger.debug('calculating average color...');
|
||||
|
||||
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 g = Math.round(info.channels[1].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}`);
|
||||
|
||||
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) { }
|
||||
};
|
||||
|
||||
@ -422,8 +422,10 @@ export default async function(
|
||||
|
||||
if (isLink) {
|
||||
file.url = url;
|
||||
file.thumbnailUrl = url;
|
||||
file.webpublicUrl = url;
|
||||
// ローカルプロキシ用
|
||||
file.accessKey = uuid();
|
||||
file.thumbnailAccessKey = 'thumbnail-' + uuid();
|
||||
file.webpublicAccessKey = 'webpublic-' + uuid();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { driveChart, perUserDriveChart, instanceChart } from '../chart';
|
||||
import { createDeleteObjectStorageFileJob } from '../../queue';
|
||||
import { fetchMeta } from '../../misc/fetch-meta';
|
||||
import { getS3 } from './s3';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export async function deleteFile(file: DriveFile, isExpired = false) {
|
||||
if (file.storedInternal) {
|
||||
@ -68,9 +69,13 @@ function postProcess(file: DriveFile, isExpired = false) {
|
||||
DriveFiles.update(file.id, {
|
||||
isLink: true,
|
||||
url: file.uri,
|
||||
thumbnailUrl: file.uri,
|
||||
webpublicUrl: file.uri,
|
||||
thumbnailUrl: null,
|
||||
webpublicUrl: null,
|
||||
size: 0,
|
||||
// ローカルプロキシ用
|
||||
accessKey: uuid(),
|
||||
thumbnailAccessKey: 'thumbnail-' + uuid(),
|
||||
webpublicAccessKey: 'webpublic-' + uuid(),
|
||||
});
|
||||
} else {
|
||||
DriveFiles.delete(file.id);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as sharp from 'sharp';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export type IImage = {
|
||||
data: Buffer;
|
||||
@ -74,29 +73,3 @@ export async function convertToPng(path: string, width: number, height: number):
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
@ -3,25 +3,27 @@ import * as Path from 'path';
|
||||
import config from '../../config';
|
||||
|
||||
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) {
|
||||
return fs.createReadStream(`${InternalStorage.path}/${key}`);
|
||||
return fs.createReadStream(InternalStorage.resolvePath(key));
|
||||
}
|
||||
|
||||
public static saveFromPath(key: string, srcPath: string) {
|
||||
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
||||
fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`);
|
||||
fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
|
||||
return `${config.url}/files/${key}`;
|
||||
}
|
||||
|
||||
public static saveFromBuffer(key: string, data: Buffer) {
|
||||
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
||||
fs.writeFileSync(`${InternalStorage.path}/${key}`, data);
|
||||
fs.writeFileSync(InternalStorage.resolvePath(key), data);
|
||||
return `${config.url}/files/${key}`;
|
||||
}
|
||||
|
||||
public static del(key: string) {
|
||||
fs.unlink(`${InternalStorage.path}/${key}`, () => {});
|
||||
fs.unlink(InternalStorage.resolvePath(key), () => {});
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,22 @@ export default async function(follower: User, followee: User, silent = false) {
|
||||
|
||||
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
|
||||
Users.decrement({ id: follower.id }, 'followingCount', 1);
|
||||
//#endregion
|
||||
@ -47,16 +63,4 @@ export default async function(follower: User, followee: User, silent = false) {
|
||||
//#endregion
|
||||
|
||||
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
Reference in New Issue
Block a user