Compare commits

...

66 Commits

Author SHA1 Message Date
syuilo
bb7edfee04 Create aiscript.ja-JP.md 2020-04-05 18:02:39 +09:00
syuilo
caa14c70ef 12.29.0 2020-04-05 18:00:23 +09:00
syuilo
3a0f72867f New Crowdin translations (#6198)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)
2020-04-05 17:56:43 +09:00
Balazs Nadasdi
10d72742f5 Ability to set header image for a Page (#6210)
* Ability to set header image for a Page

 - Add header image to Page
 - Show it on Page view
 - Show correctly it on Page list view
 - On the Page list view, pages have a light border
   to make it easier to see an image belongs to a page

* Maybe it looks better

* Use <img> instead if <x-image>

* src -> :src; set width

* Update page.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-04-05 17:55:51 +09:00
syuilo
1b9f8a87d3 chore: Update dependencies 🚀 2020-04-04 11:08:10 +09:00
syuilo
d4a630902d refactor: Use === 2020-04-04 08:46:54 +09:00
syuilo
fef5ec874b enhance(server): Log error message when internal error occured 2020-04-04 08:27:16 +09:00
syuilo
f2e347fec1 perf(client): Lazy load themes 2020-04-04 08:25:28 +09:00
tamaina
cd3c2484ee Update CHANGELOG.md 2020-04-03 23:57:57 +09:00
MeiMei
6a396ef5e3 APIリファレンスでレスポンスのスキーマを見るのにいちいち2回クリックさせられるのを修正 (#6217) 2020-04-03 23:36:13 +09:00
syuilo
eec1af1f52 Revert 2020-04-03 23:35:14 +09:00
MeiMei
99fc77b678 APメンションはaudienceじゃなくてtagを参照するなど (#6128)
* APメンションはaudienceじゃなくてtagを参照するなど

* AP/tag/Mentionではurlじゃなくてuriを提示する

* createPersonでaliasが入力された場合に対応

* AP HTMLパースでMention/Hashtag判定にtagを使うように

* fix

* indent

* use hashtag name

* fix

* URLエンコード不要だったら<>を使わないの条件が消えたたのを修正
2020-04-03 22:51:38 +09:00
MeiMei
8bb311df51 APIリファレンスのカテゴリ処理の修正 (#6218)
* APIリファレンスのカテゴリ処理の修正

* tune
2020-04-03 22:42:29 +09:00
Satsuki Yanagi
a77df249c2 i18n (#6219) 2020-04-03 22:41:18 +09:00
Acid Chicken (硫酸鶏)
2883bca257 Merge pull request #6211 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-03 21:43:09 +09:00
Acid Chicken (硫酸鶏)
dd3af6886b Update README.md [AUTOGEN] 2020-04-03 20:52:08 +09:00
tamaina
33bcf2d1ea Fix lint 2020-04-03 17:17:46 +09:00
tamaina
795fb0eb60 Pre-render ReDoc
redoc-cliはexpandResponsesは200のみとすると数値と認識されてしまい設定できないため202,204という指定にしています
2020-04-03 17:13:41 +09:00
syuilo
9e9d378bf1 feat(streaming): Add emoji added event 2020-04-02 22:17:17 +09:00
syuilo
4a6b0edce6 Update api.ja-JP.md 2020-04-02 22:09:25 +09:00
MeiMei
356225af14 Use url if available (#6214)
* Fix #6213

* other link

* fix
2020-04-02 21:59:14 +09:00
mei23
331305e6c7 lint 2020-04-02 05:41:03 +09:00
Acid Chicken (硫酸鶏)
917b9475a5 Merge pull request #6208 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-01 03:46:30 +09:00
Acid Chicken (硫酸鶏)
e895fc954b Update README.md [AUTOGEN] 2020-04-01 01:58:08 +09:00
Acid Chicken (硫酸鶏)
6d3e18a6a1 Merge pull request #6191 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-01 01:41:53 +09:00
syuilo
79354f4faf Refactoring 2020-03-31 09:15:04 +09:00
syuilo
28f8933c3c Refactoring 2020-03-31 09:11:43 +09:00
syuilo
10356b4041 Merge branch 'master' into develop 2020-03-31 08:08:22 +09:00
syuilo
6a732ab1cd Fix #6203 2020-03-31 08:07:10 +09:00
MeiMei
47322b35ff APIの権限設定漏れを修正 (#6202)
* Fix: 権限設定漏れ

* requireAdmin
2020-03-30 11:45:57 +09:00
MeiMei
4c6d0386b9 admin/accounts/createで一般ユーザーがアカウントを作成し放題なのを修正 (#6205) 2020-03-30 09:27:39 +09:00
MeiMei
a448172952 Fix #6199 (#6201) 2020-03-29 23:18:03 +09:00
MeiMei
244ef0cb8f トークン系の乱数ソースではcryptoを使うように (#6200) 2020-03-29 23:16:36 +09:00
syuilo
cc66a1f9c7 Merge branch 'develop' 2020-03-29 17:46:31 +09:00
syuilo
e2183400e5 12.28.0 2020-03-29 17:45:53 +09:00
syuilo
afc531bd26 New Crowdin translations (#6194)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)
2020-03-29 17:45:38 +09:00
syuilo
02cc1891f2 Add miauth info into meta.features 2020-03-29 17:44:14 +09:00
syuilo
09e3ddbd57 アプリの権限を確認できるように 2020-03-29 17:06:36 +09:00
syuilo
d0fff562ea Fix bug 2020-03-29 17:04:22 +09:00
syuilo
8ce5366e80 テーマ関係 2020-03-29 16:09:44 +09:00
syuilo
bfcda7cc02 Better sql log 2020-03-29 14:08:19 +09:00
syuilo
c52aeb6618 Fix type 2020-03-29 11:28:55 +09:00
syuilo
f5ebfdca61 🎨 2020-03-29 11:06:58 +09:00
syuilo
db93838729 Clean up 2020-03-29 10:59:05 +09:00
syuilo
bb835a6e8a Fix bug 2020-03-29 10:49:43 +09:00
syuilo
52feba0e3a Refactor: Use === 2020-03-29 10:39:36 +09:00
syuilo
a1076c3108 🎨 2020-03-29 10:34:46 +09:00
syuilo
bad068b20e ✌️ 2020-03-29 10:17:23 +09:00
syuilo
ec41d461c0 ✌️ 2020-03-29 10:16:32 +09:00
syuilo
a826cd6845 ✌️ 2020-03-29 10:15:33 +09:00
syuilo
a950b6193a インスタンス一覧でソートできるように 2020-03-29 10:14:33 +09:00
MeiMei
2cc4de2b23 Update CHANGELOG.md 2020-03-29 03:13:48 +09:00
MeiMei
03ef6996ff Update CHANGELOG.md 2020-03-29 03:01:45 +09:00
tamaina
d1e5def30e Update CHANGELOG.md 2020-03-29 00:41:28 +09:00
tamaina
02fbda2154 Update CHANGELOG.md 2020-03-29 00:40:08 +09:00
tamaina
c21694a24a Update CHANGELOG.md 2020-03-29 00:38:56 +09:00
tamaina
cb98336b0a Update CHANGELOG.md 2020-03-29 00:22:43 +09:00
syuilo
97d25bc6a3 Merge branch 'develop' 2020-03-28 22:35:19 +09:00
syuilo
b36a1a9d0e 12.27.1 2020-03-28 22:35:05 +09:00
syuilo
cd44ff0aaa 🎨 2020-03-28 22:25:52 +09:00
syuilo
032571c326 Update coloring 🎨 2020-03-28 22:14:13 +09:00
syuilo
6b890e3f82 Fix style 2020-03-28 22:10:14 +09:00
syuilo
9998845b21 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-03-28 22:04:26 +09:00
syuilo
7ee4385deb Fix bug 2020-03-28 22:04:23 +09:00
mei23
695277c9eb lint fix 2020-03-28 20:56:17 +09:00
Acid Chicken (硫酸鶏)
4bf1c23b3c Update README.md [AUTOGEN] 2020-03-28 04:55:08 +09:00
121 changed files with 15974 additions and 11493 deletions

View File

@@ -1,6 +1,44 @@
ChangeLog
=========
12.28.0 (2020/3/29)
-------------------
### ✨Improvements
* インストールされたアプリのページでアプリの権限を確認できるように
* API: api/meta.features.miauthを追加
MiAuthに対応しているかどうかを確認するために利用できます。
値はつねにtrueを取ります。
* インスタンス一覧でソートできるように
12.27.1 (2020/03/28)
-------------------
### ✨Improvements
* MiAuthのバグを修正
12.27.0 (2020/03/28)
-------------------
### ✨Improvements
* サードパーティーアプリケーションの認証方法にMiAuthを追加 ([Misskey API ドキュメント](https://github.com/syuilo/misskey/blob/b8088dc01a0c53b264c0697082ff5b16b06c4cda/src/docs/api.ja-JP.md#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B))
従来の、API `app/create` => `auth/session/generate` => `auth/session/userkey` を使用する方法は依然として使用可能です。
UIからアプリを作成する画面 (`/dev/apps`) は廃止されました、同等の操作を行いたい場合は API `app/create` で可能です。
MiAuthに対応しているかどうかは`api/meta.features.miauth`で確認できます12.28.0~)。
* テーマをインポートする前にプレビューできるように
* アプリから通知を作成できるように
* インストールしたアプリを見たり削除したりできるように
12.26.0 (2020/03/25)
-------------------
### ✨Improvements
* ロゴが新しく
* インスタンス設定の「ユーザー」が登録の逆順で表示されるように
### 🐛Fixes
* 新規登録フォームの「利用規約」のリンク色が通常の文字と同じだった問題を修正
* ダークモードの同期の問題を修正
12.25.0 (2020/03/24)
-------------------
@@ -25,7 +63,6 @@ ChangeLog
### 🐛Fixes
* iOSで起動できない問題を修正
* 画面が小さいとメニューがすべて見えない問題を修正
* Pages画面にタイトルがない問題を修正
12.24.0 (2020/03/22)

108
README.md
View File

@@ -109,100 +109,102 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
----------------------------------------------------------------
<!-- PATREON_START -->
<table><tr>
<td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/605366/c9dc408fdcbf412fb183ca5b06235f8d/1.jpeg?token-time=2145916800&token-hash=oaqsjLqOFjWN5I9hm2epOaTXaEtKwQUy5OW-EpAz6-g%3D" alt="Jon Leibowitz" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko " width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=20832595">Roujo</a></td>
<td><a href="https://www.patreon.com/user?u=20832595">Roujo </a></td>
<td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td>
<td><a href="https://www.patreon.com/weepjp">weepjp</a></td>
<td><a href="https://www.patreon.com/weepjp">weepjp </a></td>
<td><a href="https://www.patreon.com/jonleibowitz">Jon Leibowitz</a></td>
<td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td>
<td><a href="https://www.patreon.com/user?u=19045173">kiritan </a></td>
<td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko </a></td>
</tr></table>
<table><tr>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi " width="100"></td>
<td><img src="https://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>
<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://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://c8.patreon.com/2/200/21285325" alt="Nie(sha) " 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>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Denshi </a></td>
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
<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/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/user?u=21285325">Nie(sha) </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>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" 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>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro" 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/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1" 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/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro " 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/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1 " 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.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=28779508">S Y</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>
<td><a href="https://www.patreon.com/user?u=26340354">totokoro</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=20494440">axtuki1</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=26340354">totokoro </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=20494440">axtuki1 </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>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco " 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.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic" 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.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/takimura">takimura </a></td>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
<td><a href="https://www.patreon.com/user?u=28295158">012</a></td>
<td><a href="https://www.patreon.com/nijimiss">nafuchoco</a></td>
<td><a href="https://www.patreon.com/user?u=28295158">012 </a></td>
<td><a href="https://www.patreon.com/nijimiss">nafuchoco </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>
<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/user?u=24641572">uroco @99</a></td>
</tr></table>
<table><tr>
<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/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone" 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/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " 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/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/efertone">Efertone</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi </a></td>
<td><a href="https://www.patreon.com/efertone">Efertone </a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Tue, 17 Mar 2020 18:57:08 UTC
**Last updated:** Fri, 03 Apr 2020 11:52:08 UTC
<!-- PATREON_END -->
[backer-url]: #backers

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -9,19 +9,19 @@
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-96,166.277)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:rgb(150,208,74);"/>
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-96,498.831)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-95.9902,55.4086)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:white;"/>
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-2.64322e-11,554.256)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:white;"/>
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
<g transform="matrix(0.5,0.866025,-0.866025,0.5,192,-110.851)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:white;"/>
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -34,14 +34,16 @@ unpin: "Lösen"
copyContent: "Inhalt kopieren"
copyLink: "Link kopieren"
delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten"
deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen."
addToList: "Zur Liste hinzufügen"
sendMessage: "Nachricht senden"
copyUsername: "Benutzernamen kopieren"
reply: "Antworten"
loadMore: "Zeige mehr"
youGotNewFollower: "Sie haben einen neuen Follower"
receiveFollowRequest: "Follow Request erhalten."
followRequestAccepted: "FollowRequestAkzeptiert"
receiveFollowRequest: "Follow-Anfrage erhalten."
followRequestAccepted: "Follow-Anfrage akzeptiert"
mentions: "Erwähnungen"
directNotes: "Direktnachrichten"
importAndExport: "Importieren und Exportieren"
@@ -51,6 +53,8 @@ files: "Dateien"
download: "Download"
driveFileDeleteConfirm: "Möchtest du die Datei \"{name}\" löschen? Die zugehörige Notiz wird ebenso verschwinden."
unfollowConfirm: "Möchtest du {name} nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen"
noLists: "Keine Liste!"
note: "Notiz"
@@ -109,24 +113,33 @@ addAcount: "Benutzerkonto hinzufügen"
loginFailed: "Login fehlgeschlagen"
general: "Allgemein"
wallpaper: "Hintergrund"
setWallpaper: "Hintergrund festlegen"
removeWallpaper: "Hintergrund entfernen"
searchWith: "Suche: {q}"
youHaveNoLists: "Du hast keine Listen"
followConfirm: "Möchtest du {name} wirklich folgen?"
proxyAccount: "Proxy-Benutzerkonto"
host: "Host"
selectUser: "Benutzer wählen"
recipient: "Empfänger"
annotation: "Anmerkung"
federation: "Föderation"
instances: "Instanz"
registeredAt: "Registriert am"
latestRequestSentAt: "Letzte Anfrage gesendet am"
latestRequestReceivedAt: "Letzte Anfrage erhalten am"
latestStatus: "Neuester Status"
storageUsage: "Speicherplatzverbrauch"
charts: "Charts"
perHour: "Pro Stunde"
perDay: "Pro Tag"
stopActivityDelivery: "Senden von Aktivitäten einstellen"
blockThisInstance: "Diese Instanz blockieren"
software: "Software"
version: "Version"
metadata: "Metadaten"
withNFiles: "{n} Datei(en)"
monitor: "Beobachten"
jobQueue: "Job-Warteschlange"
cpuAndMemory: "CPU und Arbeitsspeicher"
network: "Netzwerk"
@@ -157,6 +170,7 @@ federating: "Föderiert"
blocked: "Blockiert"
suspended: "Gesperrt"
all: "Alles"
notResponding: "Antwortet nicht"
changePassword: "Passwort ändern"
security: "Sicherheit"
retypedNotMatch: "Eingaben stimmen nicht überein."
@@ -178,6 +192,10 @@ messaging: "Nachrichten"
upload: "Hochladen"
fromDrive: "Aus Drive"
fromUrl: "Von einer URL"
uploadFromUrl: "Von einer URL hochladen"
uploadFromUrlDescription: "URL der hochzuladenden Datei"
uploadFromUrlRequested: "Upload angefordert"
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis der Upload abgeschlossen ist."
explore: "Erkunden"
games: "Misskey Spiele"
messageRead: "Gelesen"
@@ -191,21 +209,32 @@ activity: "Aktivität"
images: "Bilder"
birthday: "Geburtstag"
yearsOld: "{age} Jahre alt"
registeredDate: "Registierdatum"
registeredDate: "Registrationsdatum"
location: "Ort"
theme: "Farbthemen"
themeForLightMode: "Farbthema, das im Hellmodus genutzt wird"
themeForDarkMode: "Farbthema, das im Dunkelmodus genutzt wird"
light: "Hell"
dark: "Dunkel"
lightThemes: "Helle Farbthemen"
darkThemes: "Dunkle Farbthemen"
drive: "Drive"
fileName: "Dateiname"
selectFile: "Datei auswählen"
selectFiles: "Dateien auswählen"
renameFile: "Datei umbenennen"
folderName: "Ordnername"
createFolder: "Ordner erstellen"
renameFolder: "Ordner umbenennen"
deleteFolder: "Ordner löschen"
addFile: "Datei hinzufügen"
emptyDrive: "Drive ist leer"
emptyFolder: "Der Ordner ist leer"
unableToDelete: "Nicht löschbar"
inputNewFileName: "Gib einen neuen Dateinamen ein"
inputNewFolderName: "Gib einen neuen Ordnernamen ein"
circularReferenceFolder: "Der Zielordner ist ein Unterorder des Ordners, den du verschieben möchtest."
hasChildFilesOrFolders: "Dieser Ordner kann nicht gelöscht werden, da er nicht leer ist."
copyUrl: "URL kopieren"
rename: "Umbenennen"
avatar: "Profilbild"
@@ -226,6 +255,9 @@ tosUrl: "URL der Nutzungsbedingungen"
thisYear: "Dieses Jahr"
thisMonth: "Dieser Monat"
today: "Heute"
dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Seiten"
integration: "Integration"
connectSerice: "Verbinden"
@@ -242,6 +274,7 @@ pinnedUsers: "Angepinnte Benutzer"
pinnedUsersDescription: "Gib einen Benutzernamen pro Zeile ein. Diese werden im \"Erkunden\" Tab angezeigt."
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA aktivieren"
recaptchaSecretKey: "Secret key"
antennas: "Antennen"
manageAntennas: "Antennen verwalten"
name: "Name"
@@ -249,6 +282,8 @@ antennaSource: "Antennenquelle"
antennaKeywords: "Schlüsselwörter, die beobachtet werden sollen"
antennaExcludeKeywords: "Schlüsselwörter, die ignoriert werden sollen"
antennaKeywordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen."
notifyAntenna: "Über neue Notizen benachrichtigen"
withFileAntenna: "Nur Notizen mit Dateien"
serviceworker: "ServiceWorker"
enableServiceworker: "ServiceWorker aktivieren"
antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben"
@@ -263,8 +298,24 @@ recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer"
recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer"
exploreUsersCount: "Es gibt {count} Benutzer"
exploreFediverse: "Das Fediverse erkunden"
popularTags: "Beliebte Schlagwörter"
userList: "Listen"
about: "Über"
aboutMisskey: "Über Misskey"
misskeySource: "Der Quelltext ist hier verfügbar:"
misskeyTranslation: "Hilf dabei, Misskey zu übersetzen:"
misskeyDonate: "Spende an Misskey, um die Weiterentwicklung zu unterstützen:"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
patrons: "UnterstützerInnen"
administrator: "Administrator"
twoStepAuthentication: "Zwei-Faktor-Authentifizierung"
moderator: "Moderator"
nUsersMentioned: "{n} Benutzer erwähnt"
securityKey: "Sicherheitsschlüssel"
securityKeyName: "Schlüsselname"
registerSecurityKey: "Sicherheitsschlüssel registrieren"
lastUsed: "Zuletzt benutzt"
passwordLessLogin: "Passwortloses Anmelden einrichten"
resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist \"{password}\""
posted: "Gesendet"
@@ -279,7 +330,18 @@ uploadFolder: "Standardordner für Uploads"
cacheClear: "Cache leeren"
markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren"
markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren"
help: "Hilfe"
inputMessageHere: "Hier Nachricht eingeben"
close: "Schließen"
group: "Gruppe"
groups: "Gruppen"
createGroup: "Gruppe erstellen"
ownedGroups: "Eigene Gruppen"
joinedGroups: "Beigetretene Gruppen"
invites: "Einladen"
groupName: "Gruppenname"
members: "Mitglieder"
transfer: "Übertragen"
title: "Betreff"
text: "Text"
enable: "Aktivieren"
@@ -288,12 +350,40 @@ retype: "Erneut eingeben"
noteOf: "Notiz von {user}"
inviteToGroup: "Zu Gruppe einladen"
maxNoteTextLength: "Maximale Länge von Notizen"
quoteAttached: "Zitiert"
quoteQuestion: "Als Zitat anfügen?"
noMessagesYet: "Noch keine Nachrichten"
newMessageExists: "Du hast eine neue Nachricht"
onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden"
signinRequired: "Anmeldung erforderlich"
invitationCode: "Einladungscode"
checking: "Wird überprüft..."
available: "Verfügbar"
unavailable: "Unverfügbar"
usernameInvalidFormat: "Buchstaben, Zahlen und Unterstriche sind verwendbar."
tooShort: "Zu kurz"
tooLong: "Zu lang"
weakPassword: "Schwaches Passwort"
normalPassword: "Standardpasswort"
strongPassword: "Starkes Passwort"
passwordMatched: "Stimmt überein"
passwordNotMatched: "Stimmt nicht überein"
signinWith: "Mit {x} anmelden"
signinFailed: "Anmeldung fehlgeschlagen. Überprüfe Benutzername und Passswort."
tapSecurityKey: "Tippe deinen Sicherheitsschlüssel an"
or: "Oder"
uiLanguage: "Sprache der Benutzeroberfläche"
groupInvited: "Du wurdest in eine Gruppe eingeladen"
aboutX: "Über {x}"
useOsNativeEmojis: "Eingebaute Emojis des Betriebssystems benutzen"
youHaveNoGroups: "Keine Gruppen vorhanden"
joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene."
noHistory: "Kein Verlauf"
disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren"
doing: "In Bearbeitung"
category: "Kategorie"
tags: "Schlagwörter"
docSource: "Quelle dieses Dokuments"
createAccount: "Benutzerkonto erstellen"
existingAcount: "Bestehendes Benutzerkonto"
regenerate: "Regenerieren"
@@ -312,14 +402,43 @@ promotion: "Hervorgehoben"
promote: "Hervorheben"
numberOfDays: "Anzahl der Tage"
hideThisNote: "Diese Notiz verstecken"
objectStorage: "Objektspeicher"
useObjectStorage: "Objektspeicher verwenden"
objectStorageBaseUrl: "Basis-URL"
objectStorageBucket: "Bucket"
objectStoragePrefix: "Prefix"
objectStorageEndpoint: "Endpoint"
objectStorageRegion: "Region"
objectStorageUseSSL: "SSL verwenden"
serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen"
newNoteRecived: "Du hast eine neue Notiz empfangen"
sounds: "Töne"
listen: "Anhören"
none: "Keine"
volume: "Lautstärke"
details: "Details"
chooseEmoji: "Wähle ein Emoji"
unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden."
recentUsed: "Vor kurzem verwendet"
install: "Installieren"
uninstall: "Uninstallieren"
installedApps: "Authorisierte Anwendungen"
nothing: "Hier gibt es nichts zu sehen"
installedDate: "Authorisiert"
lastUsedDate: "Zuletzt verwendet"
state: "Status"
sort: "Sortieren"
ascendingOrder: "Aufsteigende Reihenfolge"
descendingOrder: "Absteigende Reihenfolge"
_theme:
explore: "Themen erforschen"
install: "Thema installieren"
manage: "Themaverwaltung"
code: "Themencode"
installed: "{name} wurde installiert"
alreadyInstalled: "Dieses Thema ist bereits installiert"
invalid: "Themenformat ist ungültig"
_sfx:
note: "Notizen"
noteMy: "Meine Notizen"
@@ -342,11 +461,47 @@ _time:
second: "Sekunde"
minute: "Minute"
hour: "Stunde"
day: "t"
_2fa:
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert"
registerDevice: "Neues Gerät registrieren"
registerKey: "Neuen Sicherheitsschlüssel registrieren"
_permissions:
"read:account": "Deine Benutzerkontoinformationen lesen"
"write:account": "Deine Benutzerkontoinformationen bearbeiten"
"read:blocks": "Die Liste deiner blockierten Benutzer lesen"
"write:blocks": "Die Liste deiner blockierten Benutzer bearbeiten"
"read:drive": "Deine Drive-Dateien und Ordner lesen"
"write:drive": "Deine Drive-Dateien und Ordner bearbeiten oder löschen"
"read:favorites": "Deine Favoriten-Liste lesen"
"write:favorites": "Deine Favoriten-Liste bearbeiten"
"read:following": "Deine Follower-Liste lesen"
"write:following": "Anderen Benutzern folgen oder entfolgen"
"read:messaging": "Nachrichten lesen"
"write:messaging": "Nachrichten schicken oder löschen"
"read:mutes": "Stummschaltungen sehen"
"write:mutes": "Stummschaltungen bearbeiten"
"write:notes": "Notizen schreiben oder löschen"
"read:notifications": "Benachrichtigungen lesen"
"write:notifications": "Mit Benachrichtigungen arbeiten"
"read:reactions": "Reaktionen sehen"
"write:reactions": "Reaktionen hinzufügen und bearbeiten"
"write:votes": "In Umfragen abstimmen"
"read:pages": "Deine Seiten lesen"
"write:pages": "Deine Seiten bearbeiten oder löschen"
"read:page-likes": "Seiten-Likes lesen"
"write:page-likes": "Seiten-Likes bearbeiten"
"read:user-groups": "Deine Benutzergruppen lesen"
"write:user-groups": "Benutzergruppen bearbeiten oder löschen"
_auth:
shareAccess: "Möchtest du \"{name}\" authorisieren, auf dieses Benuzerkonto zugreifen zu können?"
shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?"
permissionAsk: "Diese Anwendung erfordert folgende Berechtigungen:"
pleaseGoBack: "Bitte gehe zurück zur Anwendung"
callback: "Rückkehr zur Anwendung"
denied: "Zugriff verweigert"
_antennaSources:
all: "Alle Notizen"
_weekday:
sunday: "Sonntag"
monday: "Montag"
@@ -442,31 +597,35 @@ _charts:
_instanceCharts:
requests: "Anfragen"
users: "Unterschied in der Anzahl von Benutzern"
usersTotal: "Anzahl aller Benutzer"
notes: "Unterschied in der Anzahl von Notizen"
notesTotal: "Anzahl aller Notizen"
ff: "Unterschied in der Anzahl von Followern"
ffTotal: "Gesamtanzahl der Follower"
cacheSize: "Unterschied in der Größe des Caches"
cacheSizeTotal: "Gesamtgröße des Caches"
files: "Unterschied in der Anzahl der Dateien"
filesTotal: "Gesamtanzahl der Dateien"
_timelines:
home: "Startseite"
local: "Lokal"
social: "Sozial"
global: "Global"
_pages:
viewPage: "Deine Seiten lesen"
content: "Inhalt"
title: "Titel"
url: "Seiten-URL"
summary: "Zusammenfassung"
alignCenter: "Mittig ausrichten"
hideTitleWhenPinned: "Seitentitel ausblenden, wenn an dein Profil angepinnt "
font: "Schriftart"
fontSerif: "Serif"
fontSansSerif: "Sans Serif"
eyeCatchingImageSet: "Vorschaubild festlegen"
eyeCatchingImageRemove: "Vorschaubild entfernen"
chooseBlock: "Block hinzufügen"
selectType: "Wähle einen Typ"
enterVariableName: "Gib einen Namen für deine Variable ein"
variableNameIsAlreadyUsed: "Dieser Name wird bereits von einer anderen Variable verwendet"
contentBlocks: "Inhalt"
inputBlocks: "Eingabe"
specialBlocks: "Spezial"
blocks:
text: "Text"
textarea: "Textfeld"
@@ -481,22 +640,27 @@ _pages:
text: "Inhalt"
textInput: "Texteingabe"
_textInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
textareaInput: "Eingabe des mehrzeiligen Textfelds"
_textareaInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
numberInput: "Nummereingabe"
_numberInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
switch: "Fallunterscheidung"
_switch:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
counter: "Zähler"
_counter:
name: "Variablenname"
text: "Titel"
inc: "Erhöhen um"
_button:
@@ -516,6 +680,7 @@ _pages:
no-variable: "Keine"
radioButton: "Optionsfeld"
_radioButton:
name: "Variablenname"
title: "Titel"
values: "Auswahlmöglichkeiten (getrennt durch Zeilenumbrüche)"
default: "Standardwert"
@@ -671,6 +836,7 @@ _pages:
splitStrByLine: "Text nach Zeilenumbrüchen aufteilen"
_splitStrByLine:
arg1: "Text"
ref: "Variablen"
fn: "Funktionen"
_fn:
arg1: "Ausgabe"

View File

@@ -468,6 +468,14 @@ unableToProcess: "The operation could not be completed."
recentUsed: "Recently used"
install: "Install"
uninstall: "Uninstall"
installedApps: "Authorized Applications"
nothing: "There's nothing to see here"
installedDate: "Authorized"
lastUsedDate: "Last used"
state: "State"
sort: "Sort"
ascendingOrder: "Ascending"
descendingOrder: "Descending"
_theme:
explore: "Explore Themes"
install: "Install theme"
@@ -560,7 +568,11 @@ _permissions:
"write:user-groups": "Edit or delete user groups"
_auth:
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
permissionAsk: "This application requires following permissions:"
pleaseGoBack: "Please go back to the application"
callback: "Returning back to the application"
denied: "Access Denied"
_antennaSources:
all: "All notes"
homeTimeline: "Notes from following users"
@@ -664,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Requests"
users: "Difference in # of users"
usersTotal: "Total # of users"
usersTotal: "Cumulative total # of users"
notes: "Difference in # of notes"
notesTotal: "Total # of notes"
notesTotal: "Cumulative total # of notes"
ff: "Difference in # of followers"
ffTotal: "Total # of followers"
ffTotal: "Cumulative total # of followers"
cacheSize: "Difference in cache size"
cacheSizeTotal: "Total accumulated cache"
cacheSizeTotal: "Cumulative total cache size"
files: "Difference in # of files"
filesTotal: "Total # of files"
filesTotal: "Cumulative total # of files"
_timelines:
home: "Home"
local: "Local"

View File

@@ -468,6 +468,14 @@ unableToProcess: "La operación no se puede llevar a cabo"
recentUsed: "Usado recientemente"
install: "Instalación"
uninstall: "Desinstalar"
installedApps: "Aplicaciones Autorizadas"
nothing: "No hay nada que ver aqui"
installedDate: "Autorizado"
lastUsedDate: "Utilizado el"
state: "Estado"
sort: "Ordenar"
ascendingOrder: "Ascendente"
descendingOrder: "Descendente"
_theme:
explore: "Explorar temas"
install: "Instalar tema"
@@ -560,7 +568,11 @@ _permissions:
"write:user-groups": "Administrar grupos de usuarios"
_auth:
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?"
permissionAsk: "Esta aplicación requiere los siguientes permisos"
pleaseGoBack: "Por favor, vuelve a la aplicación"
callback: "Volviendo a la aplicación"
denied: "Acceso denegado"
_antennaSources:
all: "Todas las notas"
homeTimeline: "Notas de los usuarios que sigues"
@@ -664,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Pedidos"
users: "Variación de usuarios"
usersTotal: "Total de usuarios"
usersTotal: "Total acumulado de usuarios"
notes: "Variación de la cantidad de notas"
notesTotal: "Estimación de notas"
notesTotal: "Total acumulado de la cantidad de notas"
ff: "Variación de cantidad de seguidos/seguidores"
ffTotal: "Total de seguidos/seguidores"
ffTotal: "Total acumulado de cantidad de seguidos/seguidores"
cacheSize: "Variación del tamaño de la caché"
cacheSizeTotal: "Total del tamaño de la caché"
cacheSizeTotal: "Total acumulado del tamaño de la caché"
files: "Variación de cantidad de archivos"
filesTotal: "Total de archivos"
filesTotal: "Total acumulado de cantidad de archivos"
_timelines:
home: "Inicio"
local: "Local"

View File

@@ -42,8 +42,8 @@ sendMessage: "Envoyer un message"
copyUsername: "Copier le nom d'utilisateur"
reply: "Répondre"
loadMore: "Voir plus"
youGotNewFollower: "Vous a abonnés"
receiveFollowRequest: "Demande de abonnés reçue"
youGotNewFollower: "Vous suit"
receiveFollowRequest: "Demande de suivi reçue"
followRequestAccepted: "L'abonne la demande acceptée"
mentions: "Mentions"
directNotes: "Messages directs"
@@ -53,7 +53,7 @@ export: "Exporter"
files: "Fichier·s"
download: "Télécharger"
driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier \"{name}\" ? Les notes avec ce fichier joint seront aussi supprimées."
unfollowConfirm: "Êtes-vous sûr·de ne plus vouloir abonne {name} ?"
unfollowConfirm: "Se désabonner de {name} ?"
exportRequested: "Vous avez demandé une exportation. Cela pourrait prendre un peu de temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps."
lists: "Listes"
@@ -62,17 +62,17 @@ note: "Note"
notes: "Notes"
following: "Abonnements"
followers: "Abonné·e·s"
followsYou: "Votre abonné"
followsYou: "Vous suit"
createList: "Créer une liste"
manageLists: "Gérer les listes"
error: "Une erreur est survenue"
retry: "Réessayer"
enterListName: "Nom de la liste"
privacy: "Vie privée"
makeFollowManuallyApprove: "Demandes dabonnements requiert lapprobation"
makeFollowManuallyApprove: "Demandes dsuivi requiert l'approbation"
defaultNoteVisibility: "Visibilité par défaut"
follow: "Abonnement"
followRequest: "Demande dabonnement"
follow: "Suivre"
followRequest: "Demande dsuivre"
followRequests: "Demandes dabonnement"
unfollow: "Se désabonner"
followRequestPending: "En attente dapprobation"
@@ -121,7 +121,7 @@ setWallpaper: "Définir le fond d'écran"
removeWallpaper: "Supprimer l'arrière plan"
searchWith: "Recherche : {q}"
youHaveNoLists: "Vous n'avez aucune liste"
followConfirm: "Désirez-vous abonne {name} ?"
followConfirm: "Désirez-vous suivre {name} ?"
proxyAccount: "Compte proxy"
proxyAccountDescription: "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 notes ne serait pas visibles sur l'instance si personne ne le·la abonné. Le compte proxy va donc le·la abonne pour que ses notes soient acheminées."
host: "Hôte"
@@ -468,6 +468,14 @@ unableToProcess: "L'opération n'a pas pu être complétée"
recentUsed: "Récemment utilisé"
install: "Installation"
uninstall: "Désinstaller"
installedApps: "Applications Autorisées"
nothing: "Il n'y a rien à voir ici"
installedDate: "Autorisé"
lastUsedDate: "Dernière utilisation"
state: "État"
sort: "Trier"
ascendingOrder: "Ascendant"
descendingOrder: "Descendant"
_theme:
explore: "Explorer les thèmes"
install: "Installer un thème"
@@ -560,7 +568,11 @@ _permissions:
"write:user-groups": "Éditer les groupes des utilisateur·rice·s"
_auth:
shareAccess: "Autoriser \"{name}\" à accéder à votre compte ?"
shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre compte?"
permissionAsk: "Cette application nécessite les autorisations suivantes "
pleaseGoBack: "Veillez retourner à l'application"
callback: "Retour vers lapplication"
denied: "Accès refusé"
_antennaSources:
all: "Toutes les notes"
homeTimeline: "Notes de l'utilisateur auquel je m'abonne"
@@ -651,7 +663,7 @@ _charts:
federationInstancesIncDec: "Variation du nombre d'instances"
federationInstancesTotal: "Nombre d'instances au total"
usersIncDec: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Nombre d'utilsateur·rice·s au total"
usersTotal: "Nombre d'utilisateur·rice·s au total"
activeUsers: "Utilisateur·rice·s actif·ve·s"
notesIncDec: "Variation du nombre d'notes"
localNotesIncDec: "Variation du nombre de notes local"
@@ -664,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Requêtes"
users: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Somme du nombre d'utilisateur·rice·s accumulés"
usersTotal: "Nombre d'utilisateur·rice·s au total cumulé"
notes: "Variation du nombre d'notes"
notesTotal: "Somme du nombre dnotes accumulés"
notesTotal: "Nombre d'notes au total cumulé"
ff: "Variation des abonné·e·s"
ffTotal: "Somme du nombre d'abonnements accumulés"
ffTotal: "Nombre d'abonné·e·s au total cumulé"
cacheSize: "Variation de la taille du cache"
cacheSizeTotal: "Somme de la taille du cache accumulé"
cacheSizeTotal: "La taille du cache au total cumulé"
files: "Variation du nombre de fichiers"
filesTotal: "Somme du nombre de fichiers accumulés"
filesTotal: "Nombre de fichiers au total cumulé"
_timelines:
home: "Principal"
local: "Local"

View File

@@ -472,6 +472,10 @@ installedApps: "インストールされたアプリ"
nothing: "ありません"
installedDate: "インストール日時"
lastUsedDate: "最終使用日時"
state: "状態"
sort: "ソート"
ascendingOrder: "昇順"
descendingOrder: "降順"
_theme:
explore: "テーマを探す"
@@ -691,15 +695,15 @@ _charts:
_instanceCharts:
requests: "リクエスト"
users: "ユーザーの増減"
usersTotal: "ユーザーの積"
usersTotal: "ユーザーの積"
notes: "ノートの増減"
notesTotal: "ノートの積"
notesTotal: "ノートの積"
ff: "フォロー/フォロワーの増減"
ffTotal: "フォロー/フォロワーの積"
ffTotal: "フォロー/フォロワーの積"
cacheSize: "キャッシュサイズの増減"
cacheSizeTotal: "キャッシュサイズの積"
cacheSizeTotal: "キャッシュサイズの積"
files: "ファイル数の増減"
filesTotal: "ファイル数の積"
filesTotal: "ファイル数の積"
_timelines:
home: "ホーム"

View File

@@ -468,6 +468,14 @@ unableToProcess: "작업을 완료할 수 없습니다"
recentUsed: "최근 사용"
install: "설치"
uninstall: "삭제"
installedApps: "인증된 애플리케이션"
nothing: "아무것도 없습니다"
installedDate: "승인한 날짜"
lastUsedDate: "마지막 사용"
state: "상태"
sort: "정렬"
ascendingOrder: "오름차순"
descendingOrder: "내림차순"
_theme:
explore: "테마 찾아보기"
install: "테마 설치"
@@ -560,7 +568,11 @@ _permissions:
"write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다"
_auth:
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?"
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
callback: "앱으로 돌아갑니다"
denied: "접근이 거부되었습니다"
_antennaSources:
all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트"
@@ -649,26 +661,26 @@ _exportOrImport:
userLists: "리스트"
_charts:
federationInstancesIncDec: "연합 인스턴스 수 증감"
federationInstancesTotal: "연합 인스턴스 수"
federationInstancesTotal: "연합 인스턴스 수 합계"
usersIncDec: "유저 수 증감"
usersTotal: "유저 수 합계"
activeUsers: "활성 유저 수"
notesIncDec: "노트 수 증감"
localNotesIncDec: "로컬 노트 수 증감"
remoteNotesIncDec: "리모트 노트 수 증감"
notesTotal: "노트 수"
notesTotal: "노트 수 합계"
filesIncDec: "파일 수 증감"
filesTotal: "파일 수"
filesTotal: "파일 수 합계"
storageUsageIncDec: "스토리지 사용량 증감"
storageUsageTotal: "스토리지 사용량"
storageUsageTotal: "스토리지 사용량 합계"
_instanceCharts:
requests: "요청"
users: "유저 수 증감"
usersTotal: "누적 유저 수"
notes: "노트 수 증감"
notesTotal: " 노트 수"
notesTotal: "누적 노트 수"
ff: "팔로잉/팔로워 증감"
ffTotal: "팔로잉/팔로워 누적"
ffTotal: "누적 팔로잉/팔로워 "
cacheSize: "캐시 용량 증감"
cacheSizeTotal: "누적 캐시 용량"
files: "파일 수 증감"

View File

@@ -230,8 +230,8 @@ location: "位置"
theme: "主题"
themeForLightMode: "在轻便模式下使用的主题"
themeForDarkMode: "在黑暗模式下使用的主题"
light: "轻便"
dark: "黑暗"
light: "浅色"
dark: "深色"
lightThemes: "亮色主题"
darkThemes: "暗色主题"
syncDeviceDarkMode: "将黑暗模式与设备设置同步"
@@ -468,6 +468,14 @@ unableToProcess: "操作无法完成"
recentUsed: "最近使用"
install: "安装"
uninstall: "卸载"
installedApps: "已授权的应用"
nothing: "没什么"
installedDate: "授权日期"
lastUsedDate: "最近使用"
state: "状态"
sort: "排序"
ascendingOrder: "升序"
descendingOrder: "降序"
_theme:
explore: "寻找主题"
install: "安装主题"
@@ -478,7 +486,7 @@ _theme:
invalid: "主题格式错误"
_sfx:
note: "帖子"
noteMy: "我的笔记"
noteMy: "我的帖子"
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
@@ -560,7 +568,11 @@ _permissions:
"write:user-groups": "操作用户组"
_auth:
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
permissionAsk: "这个应用程序需要以下权限"
pleaseGoBack: "请返回到应用程序"
callback: "回到应用程序"
denied: "拒绝访问"
_antennaSources:
all: "所有帖子"
homeTimeline: "已关注用户的帖子"
@@ -664,15 +676,15 @@ _charts:
_instanceCharts:
requests: "请求"
users: "用户数量:增加/减少"
usersTotal: "用户总"
usersTotal: "用户总"
notes: "帖子:增加/减少"
notesTotal: "帖子:总数"
notesTotal: "帖子总计"
ff: "关注/被关注:数量变化"
ffTotal: "关注/被关注:总数"
ffTotal: "关注/被关注者总计"
cacheSize: "缓存大小:增加/减少"
cacheSizeTotal: "合计缓存大小"
cacheSizeTotal: "缓存大小总计"
files: "文件总数增减"
filesTotal: "合计文件总数"
filesTotal: "文件数总计"
_timelines:
home: "首页"
local: "本地"

View File

@@ -0,0 +1,15 @@
/* tslint:disable:quotemark class-name indent */
import {MigrationInterface, QueryRunner} from "typeorm";
export class apUrl1585772678853 implements MigrationInterface {
name = 'apUrl1585772678853'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.27.0",
"version": "12.29.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -11,11 +11,13 @@
"private": true,
"scripts": {
"start": "node ./index.js",
"start-product": "cross-env NODE_ENV=production node ./index.js",
"init": "npm run migrate",
"ormconfig": "node ./built/ormconfig.js",
"migrate": "ts-node ./node_modules/typeorm/cli.js migration:run",
"migrateandstart": "npm run migrate && npm run start",
"build": "webpack && gulp build",
"build-product": "cross-env NODE_ENV=production webpack && gulp build",
"webpack": "webpack",
"watch": "webpack --watch",
"gulp": "gulp build",
@@ -30,18 +32,18 @@
"lodash": "^4.17.13"
},
"dependencies": {
"@elastic/elasticsearch": "7.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.27",
"@fortawesome/free-brands-svg-icons": "5.12.1",
"@fortawesome/free-regular-svg-icons": "5.12.1",
"@fortawesome/free-solid-svg-icons": "5.12.1",
"@elastic/elasticsearch": "7.6.1",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-brands-svg-icons": "5.13.0",
"@fortawesome/free-regular-svg-icons": "5.13.0",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"@fortawesome/vue-fontawesome": "0.1.9",
"@juggle/resize-observer": "3.0.2",
"@juggle/resize-observer": "3.1.3",
"@koa/cors": "3.0.0",
"@koa/multer": "2.0.2",
"@koa/router": "8.0.8",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.12.0",
"@types/bull": "3.12.1",
"@types/cbor": "5.0.0",
"@types/dateformat": "3.0.1",
"@types/double-ended-queue": "2.1.1",
@@ -51,10 +53,10 @@
"@types/gulp-rename": "0.0.33",
"@types/gulp-replace": "0.0.31",
"@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.2",
"@types/jsdom": "12.2.4",
"@types/js-yaml": "3.12.3",
"@types/jsdom": "16.2.0",
"@types/katex": "0.11.0",
"@types/koa": "2.11.1",
"@types/koa": "2.11.3",
"@types/koa-bodyparser": "4.3.0",
"@types/koa-compress": "2.0.9",
"@types/koa-cors": "0.0.0",
@@ -68,8 +70,8 @@
"@types/koa__router": "8.0.2",
"@types/lolex": "5.1.0",
"@types/markdown-it": "0.0.9",
"@types/mocha": "7.0.1",
"@types/node": "13.7.1",
"@types/mocha": "7.0.2",
"@types/node": "13.11.0",
"@types/nodemailer": "6.4.0",
"@types/nprogress": "0.2.0",
"@types/oauth": "0.9.1",
@@ -80,7 +82,7 @@
"@types/qrcode": "1.3.4",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.15",
"@types/redis": "2.8.17",
"@types/rename": "1.0.1",
"@types/request": "2.48.4",
"@types/request-promise-native": "1.0.17",
@@ -93,26 +95,26 @@
"@types/systeminformation": "3.54.1",
"@types/tinycolor2": "1.4.2",
"@types/tmp": "0.1.0",
"@types/uuid": "3.4.7",
"@types/uuid": "7.0.2",
"@types/web-push": "3.3.0",
"@types/webpack": "4.41.6",
"@types/webpack": "4.41.10",
"@types/webpack-stream": "3.2.10",
"@types/websocket": "1.0.0",
"@types/ws": "7.2.1",
"@typescript-eslint/parser": "2.19.2",
"@types/ws": "7.2.3",
"@typescript-eslint/parser": "2.26.0",
"agentkeepalive": "4.1.0",
"animejs": "3.1.0",
"apexcharts": "3.15.6",
"apexcharts": "3.17.1",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"aws-sdk": "2.617.0",
"aws-sdk": "2.653.0",
"bcryptjs": "2.4.3",
"bull": "3.12.1",
"bull": "3.13.0",
"cafy": "15.2.1",
"cbor": "5.0.1",
"chai": "4.2.0",
"chalk": "3.0.0",
"chalk": "4.0.0",
"chart.js": "2.9.3",
"cli-highlight": "2.1.4",
"commander": "4.1.1",
@@ -124,16 +126,16 @@
"diskusage": "1.1.3",
"double-ended-queue": "2.1.0-0",
"eslint": "6.8.0",
"eslint-plugin-vue": "6.1.2",
"eslint-plugin-vue": "6.2.2",
"eventemitter3": "4.0.0",
"feed": "4.1.0",
"fibers": "4.0.2",
"file-type": "14.1.2",
"file-type": "14.1.4",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.6",
"gulp": "4.0.2",
"gulp-clean-css": "4.2.0",
"gulp-dart-sass": "0.9.1",
"gulp-clean-css": "4.3.0",
"gulp-dart-sass": "1.0.0",
"gulp-mocha": "7.0.2",
"gulp-rename": "2.0.0",
"gulp-replace": "1.0.0",
@@ -143,21 +145,21 @@
"gulp-typescript": "5.0.1",
"hard-source-webpack-plugin": "0.13.1",
"html-minifier": "4.0.0",
"http-signature": "1.3.1",
"http-signature": "1.3.4",
"https-proxy-agent": "5.0.0",
"insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0",
"is-svg": "4.2.1",
"js-yaml": "3.13.1",
"jsdom": "16.1.0",
"json5": "2.1.1",
"jsdom": "16.2.2",
"json5": "2.1.2",
"json5-loader": "3.0.0",
"jsrsasign": "8.0.12",
"jsrsasign": "8.0.13",
"katex": "0.11.1",
"koa": "2.11.0",
"koa-bodyparser": "4.2.1",
"koa-bodyparser": "4.3.0",
"koa-compress": "3.0.0",
"koa-favicon": "2.0.1",
"koa-favicon": "2.1.0",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.1",
"koa-mount": "4.0.0",
@@ -165,24 +167,23 @@
"koa-slow": "2.1.0",
"koa-views": "6.2.1",
"langmap": "0.0.16",
"loader-utils": "1.2.3",
"lolex": "5.1.2",
"lookup-dns-cache": "2.1.0",
"markdown-it": "10.0.0",
"markdown-it-anchor": "5.2.5",
"mocha": "7.0.1",
"markdown-it-anchor": "5.2.7",
"mocha": "7.1.1",
"moji": "0.5.1",
"ms": "2.1.2",
"multer": "1.4.2",
"nested-property": "1.0.4",
"node-fetch": "2.6.0",
"nodemailer": "6.4.2",
"nodemailer": "6.4.6",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"os-utils": "0.0.14",
"parse5": "5.1.1",
"parsimmon": "1.13.0",
"pg": "7.18.1",
"pg": "8.0.0",
"portal-vue": "2.1.7",
"portscanner": "2.2.0",
"postcss-loader": "3.0.0",
@@ -193,11 +194,11 @@
"promise-sequential": "1.1.1",
"pug": "2.0.4",
"punycode": "2.1.1",
"pureimage": "0.1.6",
"pureimage": "0.2.1",
"qrcode": "1.4.4",
"random-seed": "0.3.0",
"randomcolor": "0.5.4",
"ratelimiter": "3.4.0",
"ratelimiter": "3.4.1",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.4.0",
"redis": "3.0.2",
@@ -211,57 +212,57 @@
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.25.0",
"sass": "1.26.3",
"sass-loader": "8.0.2",
"seedrandom": "3.0.5",
"sharp": "0.24.0",
"sharp": "0.25.2",
"showdown": "1.9.1",
"showdown-highlightjs-extension": "0.1.2",
"speakeasy": "2.0.0",
"stringz": "2.0.0",
"stringz": "2.1.0",
"style-loader": "1.1.3",
"summaly": "2.3.1",
"syslog-pro": "1.0.0",
"systeminformation": "4.21.2",
"systeminformation": "4.23.1",
"syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "2.3.4",
"terser-webpack-plugin": "2.3.5",
"textarea-caret": "3.1.0",
"three": "0.113.2",
"three": "0.115.0",
"tinycolor2": "1.4.1",
"tmp": "0.1.0",
"ts-loader": "6.2.1",
"ts-node": "8.6.2",
"tslint": "6.0.0",
"ts-loader": "6.2.2",
"ts-node": "8.8.1",
"tslint": "6.1.1",
"tslint-sonarts": "1.9.0",
"typeorm": "0.2.22",
"typescript": "3.7.5",
"typeorm": "0.2.24",
"typescript": "3.8.3",
"ulid": "2.3.0",
"url-loader": "3.0.0",
"uuid": "3.4.0",
"uuid": "7.0.3",
"v-animate-css": "0.0.3",
"v-debounce": "0.1.2",
"vue": "2.6.11",
"vue-color": "2.7.0",
"vue-color": "2.7.1",
"vue-content-loading": "1.6.0",
"vue-cropperjs": "4.0.1",
"vue-i18n": "8.15.3",
"vue-i18n": "8.16.0",
"vue-json-pretty": "1.6.3",
"vue-loader": "15.9.0",
"vue-loader": "15.9.1",
"vue-marquee-text-component": "1.1.1",
"vue-meta": "2.3.2",
"vue-meta": "2.3.3",
"vue-prism-component": "1.1.1",
"vue-router": "3.1.5",
"vue-router": "3.1.6",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.4.5",
"vue-svg-inline-loader": "1.5.0",
"vue-template-compiler": "2.6.11",
"vuedraggable": "2.23.2",
"vuex": "3.1.2",
"vuex-persistedstate": "2.7.1",
"vuex": "3.1.3",
"vuex-persistedstate": "3.0.1",
"web-push": "3.4.3",
"webpack": "4.41.6",
"webpack": "4.42.1",
"webpack-cli": "3.3.11",
"websocket": "1.0.31",
"ws": "7.2.1",
"ws": "7.2.3",
"xev": "2.0.1"
},
"devDependencies": {

View File

@@ -99,7 +99,7 @@ async function isPortAvailable(port: number): Promise<boolean> {
function showEnvironment(): void {
const env = process.env.NODE_ENV;
const logger = bootLogger.createSubLogger('env');
logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
if (env !== 'production') {
logger.warn('The environment is not in production mode.');

View File

@@ -310,7 +310,7 @@ export default Vue.extend({
title: this.$t('search'),
input: true
}).then(async ({ canceled, result: query }) => {
if (canceled || query == null || query == '') return;
if (canceled || query == null || query === '') return;
this.searching = true;
search(this, query).finally(() => {
@@ -320,7 +320,7 @@ export default Vue.extend({
},
searchKeypress(e) {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
this.searchWait = true;
search(this, this.searchQuery).finally(() => {
this.searchWait = false;
@@ -975,6 +975,10 @@ export default Vue.extend({
&:not(.naked) {
background: var(--pageBg);
}
&.naked {
background: var(--bg);
}
}
}

View File

@@ -18,7 +18,7 @@
</style>
</head>
<body>
<redoc spec-url='/api.json'></redoc>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

View File

@@ -1,8 +1,8 @@
<template>
<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
<slot :item="item"></slot>
<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'">
<p class="date">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>

View File

@@ -517,11 +517,11 @@ export default Vue.extend({
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
}, (this.appearNote.url || this.appearNote.uri) ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
window.open(this.appearNote.url || this.appearNote.uri, '_blank');
}
} : undefined,
null,
@@ -585,11 +585,11 @@ export default Vue.extend({
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
}, (this.appearNote.url || this.appearNote.uri) ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
window.open(this.appearNote.url || this.appearNote.uri, '_blank');
}
} : undefined]
.filter(x => x !== undefined);

View File

@@ -35,6 +35,7 @@ export default Vue.extend({
border: solid var(--lineWidth) var(--urlPreviewBorder);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--divider);
&:hover {
text-decoration: none;
@@ -42,9 +43,8 @@ export default Vue.extend({
}
> .thumbnail {
position: absolute;
width: 100px;
height: 100%;
width: 100%;
height: 200px;
background-position: center;
background-size: cover;
display: flex;

View File

@@ -12,9 +12,9 @@
<template #prefix><fa :icon="faLock"/></template>
</mk-input>
<mk-button type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button>
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="faTwitter"/> {{ $t('signinWith', { x: 'Twitter' }) }}</a></p>
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="faGithub"/> {{ $t('signinWith', { x: 'GitHub' }) }}</a></p>
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="faDiscord"/> {{ $t('signinWith', { x: 'Discord' }) }}</a></p>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><fa :icon="faTwitter" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><fa :icon="faGithub" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><fa :icon="faDiscord" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Discord' }) }}</a>
</div>
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">

View File

@@ -6,7 +6,7 @@
<div class="efvhhmdq">
<div class="no-users" v-if="empty">
<p>{{ $t('no-users') }}</p>
<p>{{ $t('noUsers') }}</p>
</div>
<div class="user" v-for="user in users" :key="user.id">
<mk-avatar class="avatar" :user="user"/>

View File

@@ -18,8 +18,9 @@ import PostFormDialog from './components/post-form-dialog.vue';
import Dialog from './components/dialog.vue';
import Menu from './components/menu.vue';
import { router } from './router';
import { applyTheme, lightTheme, builtinThemes } from './theme';
import { applyTheme, lightTheme } from './theme';
import { isDeviceDarkmode } from './scripts/is-device-darkmode';
import createStore from './store';
Vue.use(Vuex);
Vue.use(VueHotkey);
@@ -134,36 +135,39 @@ document.body.setAttribute('ontouchstart', '');
// アプリ基底要素マウント
document.body.innerHTML = '<div id="app"></div>';
const os = new MiOS();
const store = createStore();
const os = new MiOS(store);
os.init(async () => {
window.addEventListener('storage', e => {
if (e.key === 'vuex') {
os.store.replaceState(JSON.parse(localStorage['vuex']));
store.replaceState(JSON.parse(localStorage['vuex']));
} else if (e.key === 'i') {
location.reload();
}
}, false)
os.store.watch(state => state.device.darkMode, darkMode => {
// TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする
const themes = builtinThemes.concat(os.store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme)));
store.watch(state => state.device.darkMode, darkMode => {
import('./theme').then(({ builtinThemes }) => {
const themes = builtinThemes.concat(store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme)));
});
});
//#region Sync dark mode
if (os.store.state.device.syncDeviceDarkMode) {
os.store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (os.store.state.device.syncDeviceDarkMode) {
os.store.commit('device/set', { key: 'darkMode', value: mql.matches });
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: mql.matches });
}
});
//#endregion
if ('Notification' in window && os.store.getters.isSignedIn) {
if ('Notification' in window && store.getters.isSignedIn) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
@@ -171,7 +175,7 @@ os.init(async () => {
}
const app = new Vue({
store: os.store,
store: store,
metaInfo: {
title: null,
titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey')
@@ -183,7 +187,7 @@ os.init(async () => {
};
},
methods: {
api: os.api,
api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }),
signout: os.signout,
new(vm, props) {
const x = new vm({
@@ -234,58 +238,63 @@ os.init(async () => {
// マウント
app.$mount('#app');
if (app.$store.getters.isSignedIn) {
os.stream.on('emojiAdded', data => {
// TODO
//store.commit('instance/set', );
});
if (store.getters.isSignedIn) {
const main = os.stream.useSharedConnection('main');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
app.$store.dispatch('mergeMe', i);
store.dispatch('mergeMe', i);
});
main.on('readAllNotifications', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
main.on('unreadNotification', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
main.on('unreadMention', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
main.on('readAllUnreadMentions', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
main.on('unreadSpecifiedNote', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
main.on('readAllUnreadSpecifiedNotes', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
main.on('readAllMessagingMessages', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
main.on('unreadMessagingMessage', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
@@ -293,13 +302,13 @@ os.init(async () => {
});
main.on('readAllAntennas', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAntenna: false
});
});
main.on('unreadAntenna', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAntenna: true
});
@@ -307,13 +316,13 @@ os.init(async () => {
});
main.on('readAllAnnouncements', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAnnouncement: false
});
});
main.on('clientSettingUpdated', x => {
app.$store.commit('settings/set', {
store.commit('settings/set', {
key: x.key,
value: x.value
});

View File

@@ -2,16 +2,11 @@ import autobind from 'autobind-decorator';
import Vue from 'vue';
import { EventEmitter } from 'eventemitter3';
import initStore from './store';
import { apiUrl, version } from './config';
import Progress from './scripts/loading';
import Stream from './scripts/stream';
//#region api requests
let spinner = null;
let pending = 0;
//#endregion
import store from './store';
/**
* Misskey Operating System
@@ -19,7 +14,7 @@ let pending = 0;
export default class MiOS extends EventEmitter {
public app: Vue;
public store: ReturnType<typeof initStore>;
public store: ReturnType<typeof store>;
/**
* A connection manager of home stream
@@ -31,6 +26,11 @@ export default class MiOS extends EventEmitter {
*/
private swRegistration: ServiceWorkerRegistration = null;
constructor(vuex: MiOS['store']) {
super();
this.store = vuex;
}
@autobind
public signout() {
this.store.dispatch('logout');
@@ -52,8 +52,6 @@ export default class MiOS extends EventEmitter {
});
};
this.store = initStore(this);
// ユーザーをフェッチしてコールバックする
const fetchme = (token, cb) => {
let me = null;
@@ -187,16 +185,19 @@ export default class MiOS extends EventEmitter {
}
// Register
this.api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
this.store.dispatch('api', {
endpoint: 'sw/register',
data: {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
}
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name == 'NotAllowedError') {
if (err.name === 'NotAllowedError') {
return;
}
@@ -214,52 +215,6 @@ export default class MiOS extends EventEmitter {
// Register service worker
navigator.serviceWorker.register(sw);
}
/**
* Misskey APIにリクエストします
* @param endpoint エンドポイント名
* @param data パラメータ
*/
@autobind
public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> {
if (++pending === 1) {
spinner = document.createElement('div');
spinner.setAttribute('id', 'wait');
document.body.appendChild(spinner);
}
const onFinally = () => {
if (--pending === 0) spinner.parentNode.removeChild(spinner);
};
const promise = new Promise((resolve, reject) => {
// Append a credential
if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token;
if (token) (data as any).i = token;
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
}
/**

View File

@@ -27,6 +27,12 @@
<div class="actions">
<button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button>
</div>
<details>
<summary>{{ $t('details') }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
</ul>
</details>
</div>
</div>
</template>

View File

@@ -1,11 +1,14 @@
<template>
<div class="mk-federation">
<portal to="icon"><fa :icon="faGlobe"/></portal>
<portal to="title">{{ $t('federation') }}</portal>
<section class="_card instances">
<div class="_title"><fa :icon="faGlobe"/> {{ $t('instances') }}</div>
<div class="_content">
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
<div class="inputs" style="display: flex;">
<mk-input v-model="host" :debounce="true" style="margin: 0; flex: 1;"><span>{{ $t('host') }}</span></mk-input>
<mk-select v-model="state" style="margin: 0;">
<mk-select v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ $t('state') }}</template>
<option value="all">{{ $t('all') }}</option>
<option value="federating">{{ $t('federating') }}</option>
<option value="subscribing">{{ $t('subscribing') }}</option>
@@ -14,11 +17,32 @@
<option value="blocked">{{ $t('blocked') }}</option>
<option value="notResponding">{{ $t('notResponding') }}</option>
</mk-select>
<mk-select v-model="sort" style="margin: 0; flex: 1;">
<template #label>{{ $t('sort') }}</template>
<option value="+pubSub">{{ $t('pubSub') }} ({{ $t('descendingOrder') }})</option>
<option value="-pubSub">{{ $t('pubSub') }} ({{ $t('ascendingOrder') }})</option>
<option value="+notes">{{ $t('notes') }} ({{ $t('descendingOrder') }})</option>
<option value="-notes">{{ $t('notes') }} ({{ $t('ascendingOrder') }})</option>
<option value="+users">{{ $t('users') }} ({{ $t('descendingOrder') }})</option>
<option value="-users">{{ $t('users') }} ({{ $t('ascendingOrder') }})</option>
<option value="+following">{{ $t('following') }} ({{ $t('descendingOrder') }})</option>
<option value="-following">{{ $t('following') }} ({{ $t('ascendingOrder') }})</option>
<option value="+followers">{{ $t('followers') }} ({{ $t('descendingOrder') }})</option>
<option value="-followers">{{ $t('followers') }} ({{ $t('ascendingOrder') }})</option>
<option value="+caughtAt">{{ $t('caughtAt') }} ({{ $t('descendingOrder') }})</option>
<option value="-caughtAt">{{ $t('caughtAt') }} ({{ $t('ascendingOrder') }})</option>
<option value="+lastCommunicatedAt">{{ $t('lastCommunicatedAt') }} ({{ $t('descendingOrder') }})</option>
<option value="-lastCommunicatedAt">{{ $t('lastCommunicatedAt') }} ({{ $t('ascendingOrder') }})</option>
<option value="+driveUsage">{{ $t('driveUsage') }} ({{ $t('descendingOrder') }})</option>
<option value="-driveUsage">{{ $t('driveUsage') }} ({{ $t('ascendingOrder') }})</option>
<option value="+driveFiles">{{ $t('driveFiles') }} ({{ $t('descendingOrder') }})</option>
<option value="-driveFiles">{{ $t('driveFiles') }} ({{ $t('ascendingOrder') }})</option>
</mk-select>
</div>
</div>
<div class="_content">
<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
<div class="instance" v-for="(instance, i) in items" :key="instance.id" @click="info(instance)">
<div class="instance" v-for="instance in items" :key="instance.id" @click="info(instance)">
<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
<div class="status">
<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>

View File

@@ -1,5 +1,8 @@
<template>
<div>
<portal to="icon"><fa :icon="faExchangeAlt"/></portal>
<portal to="title">{{ $t('jobQueue') }}</portal>
<x-queue :connection="connection" domain="inbox">
<template #title><fa :icon="faExchangeAlt"/> In</template>
</x-queue>

View File

@@ -214,6 +214,7 @@ export default Vue.extend({
width: 100%;
max-height: 512px;
object-fit: contain;
box-sizing: border-box;
}
> p {

View File

@@ -19,7 +19,7 @@
<button class="more _button" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
</button>
<x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up" reversed>
<x-list class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
<x-message :message="message" :is-group="group != null" :key="message.id"/>
</x-list>
</div>

View File

@@ -68,8 +68,8 @@ export default Vue.extend({
icon(): string {
return this.$route.query.icon;
},
permission(): string {
return this.$route.query.permission;
permission(): string[] {
return this.$route.query.permission ? this.$route.query.permission.split(',') : [];
},
},
methods: {
@@ -79,7 +79,7 @@ export default Vue.extend({
session: this.session,
name: this.name,
iconUrl: this.icon,
permission: this.permission || [],
permission: this.permission,
});
this.state = 'accepted';

View File

@@ -13,7 +13,7 @@
<x-notes v-if="showNext" ref="next" :pagination="next"/>
<hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.uri" style="margin-bottom: var(--margin)"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
<x-note :note="note" :key="note.id" :detail="true"/>
<div v-if="error">
<mk-error @retry="fetch()"/>

View File

@@ -102,6 +102,7 @@ import { blockDefs } from '../../scripts/aiscript/index';
import { ASTypeChecker } from '../../scripts/aiscript/type-checker';
import { url } from '../../config';
import { collectPageVars } from '../../scripts/collect-page-vars';
import { selectDriveFile } from '../../scripts/select-drive-file';
export default Vue.extend({
i18n,
@@ -405,9 +406,7 @@ export default Vue.extend({
},
setEyeCatchingImage() {
this.$chooseDriveFile({
multiple: false
}).then(file => {
selectDriveFile(this.$root, false).then(file => {
this.eyeCatchingImageId = file.id;
});
},

View File

@@ -5,6 +5,7 @@
<div class="_card" v-if="page" :key="page.id">
<div class="_title">{{ page.title }}</div>
<img class="header" :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId" />
<div class="_content">
<x-page :page="page"/>
</div>
@@ -115,6 +116,8 @@ export default Vue.extend({
<style lang="scss" scoped>
.xcukqgmh {
> ._card > .header {
max-width: 100%;
}
}
</style>

View File

@@ -57,9 +57,9 @@ const ignoreElemens = ['input', 'textarea'];
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
const key = e.code.toLowerCase();
return patterns.some(pattern => pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
pattern.ctrl === e.ctrlKey &&
pattern.shift === e.shiftKey &&
pattern.alt === e.altKey &&
!e.metaKey
);
}

View File

@@ -1,5 +1,5 @@
export default (input: string): string[] => {
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
const codes = aliases[input];
return Array.isArray(codes) ? codes : [codes];
} else {

View File

@@ -20,7 +20,7 @@ export default (opts) => ({
computed: {
empty(): boolean {
return this.items.length == 0 && !this.fetching && this.inited;
return this.items.length === 0 && !this.fetching && this.inited;
},
error(): boolean {

View File

@@ -47,9 +47,9 @@ export async function search(v: any, q: string) {
uri: q
});
dialog.close();
if (res.type == 'User') {
if (res.type === 'User') {
v.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
} else if (res.type === 'Note') {
v.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {

View File

@@ -68,7 +68,7 @@ export default class Stream extends EventEmitter {
*/
@autobind
private onOpen() {
const isReconnect = this.state == 'reconnecting';
const isReconnect = this.state === 'reconnecting';
this.state = 'connected';
this.emit('_connected_');
@@ -87,7 +87,7 @@ export default class Stream extends EventEmitter {
*/
@autobind
private onClose() {
if (this.state == 'connected') {
if (this.state === 'connected') {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
@@ -100,7 +100,7 @@ export default class Stream extends EventEmitter {
private onMessage(message) {
const { type, body } = JSON.parse(message.data);
if (type == 'channel') {
if (type === 'channel') {
const id = body.id;
let connections: Connection[];

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { apiUrl } from './config';
const defaultSettings = {
tutorial: 0,
@@ -57,13 +56,15 @@ function copy<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}
export default (os: MiOS) => new Vuex.Store({
export default () => new Vuex.Store({
plugins: [createPersistedState({
paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
})],
state: {
i: null,
pendingApiRequestsCount: 0,
spinner: null
},
getters: {
@@ -121,6 +122,47 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('settings/init', me.clientData);
}
},
api(ctx, { endpoint, data, token }) {
if (++ctx.state.pendingApiRequestsCount === 1) {
// TODO: spinnerの表示はstoreでやらない
ctx.state.spinner = document.createElement('div');
ctx.state.spinner.setAttribute('id', 'wait');
document.body.appendChild(ctx.state.spinner);
}
const onFinally = () => {
if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner);
};
const promise = new Promise((resolve, reject) => {
// Append a credential
if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token;
if (token) (data as any).i = token;
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
},
modules: {
@@ -139,9 +181,12 @@ export default (os: MiOS) => new Vuex.Store({
actions: {
async fetch(ctx) {
const meta = await os.api('meta', {
detail: false
});
const meta = await ctx.dispatch('api', {
endpoint: 'meta',
data: {
detail: false
}
}, { root: true });
ctx.commit('set', meta);
}
@@ -212,7 +257,7 @@ export default (os: MiOS) => new Vuex.Store({
},
updateWidget(state, x) {
const w = state.widgets.find(w => w.id == x.id);
const w = state.widgets.find(w => w.id === x.id);
if (w) {
w.data = x.data;
}
@@ -246,10 +291,13 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('set', x);
if (ctx.rootGetters.isSignedIn) {
os.api('i/update-client-setting', {
name: x.key,
value: x.value
});
ctx.dispatch('api', {
endpoint: 'i/update-client-setting',
data: {
name: x.key,
value: x.value
}
}, { root: true });
}
},
}

View File

@@ -27,7 +27,7 @@ export const builtinThemes = [
require('./themes/danboard.json5'),
require('./themes/olive.json5'),
require('./themes/tweetdeck.json5'),
];
] as Theme[];
let timeout = null;
@@ -66,16 +66,21 @@ export function applyTheme(theme: Theme, persist = true) {
}
}
function compile(theme: Theme): { [key: string]: string } {
function getColor(code: string): tinycolor.Instance {
// ref
if (code[0] == '@') {
return getColor(theme.props[code.substr(1)]);
function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
// ref (prop)
if (val[0] === '@') {
return getColor(theme.props[val.substr(1)]);
}
// ref (const)
else if (val[0] === '$') {
return getColor(theme.props[val]);
}
// func
if (code[0] == ':') {
const parts = code.split('<');
else if (val[0] === ':') {
const parts = val.split('<');
const func = parts.shift().substr(1);
const arg = parseFloat(parts.shift());
const color = getColor(parts.join('<'));
@@ -87,12 +92,15 @@ function compile(theme: Theme): { [key: string]: string } {
}
}
return tinycolor(code);
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = genValue(getColor(v));
}

View File

@@ -51,14 +51,14 @@ export default Vue.extend({
weekday: date.getDay()
};
d.v = peak == 0 ? 0 : d.total / (peak / 2);
d.v = peak === 0 ? 0 : d.total / (peak / 2);
if (d.v > 1) d.v = 1;
const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170;
const cs = d.v * 100;
const cl = 15 + ((1 - d.v) * 80);
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
if (d.date.weekday == 0) x--;
if (d.date.weekday === 0) x--;
});
}
});

View File

@@ -60,7 +60,7 @@ export default define({
},
methods: {
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;
@@ -68,7 +68,7 @@ export default define({
this.save();
},
toggleView() {
if (this.props.view == 1) {
if (this.props.view === 1) {
this.props.view = 0;
} else {
this.props.view++;

View File

@@ -65,7 +65,7 @@ export default define({
},
methods: {
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;
@@ -102,7 +102,7 @@ export default define({
this.monthP = monthNumer / monthDenom * 100;
this.yearP = yearNumer / yearDenom * 100;
this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
}
}
});

View File

@@ -5,7 +5,7 @@
<div class="otgbylcu">
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
</div>
</mk-container>
</div>
@@ -84,6 +84,7 @@ export default define({
border: none;
border-bottom: solid var(--lineWidth) var(--faceDivider);
border-radius: 0;
box-sizing: border-box;
}
> button {
@@ -94,22 +95,8 @@ export default define({
margin: 0;
padding: 0 10px;
height: 28px;
color: #fff;
background: var(--accent) !important;
outline: none;
border: none;
border-radius: 4px;
transition: background 0.1s ease;
cursor: pointer;
&:hover {
background: var(--accentLighten10) !important;
}
&:active {
background: var(--accentDarken) !important;
transition: background 0s ease;
}
&:disabled {
opacity: 0.7;

View File

@@ -1,6 +1,6 @@
<template>
<div>
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design == 2">
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design === 2">
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
<div class="">
@@ -66,7 +66,7 @@ export default define({
},
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;

View File

@@ -15,7 +15,7 @@ const dir = `${__dirname}/../../.config`;
/**
* Path of configuration file
*/
const path = process.env.NODE_ENV == 'test'
const path = process.env.NODE_ENV === 'test'
? `${dir}/test.yml`
: `${dir}/default.yml`;

View File

@@ -69,7 +69,9 @@ class MyCustomLogger implements Logger {
}
public logQuery(query: string, parameters?: any[]) {
sqlLogger.info(this.highlight(query));
if (program.verbose) {
sqlLogger.info(this.highlight(query));
}
}
public logQueryError(error: string, query: string, parameters?: any[]) {
@@ -158,7 +160,7 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false)
} catch (e) {}
}
const log = program.verbose;
const log = process.env.NODE_ENV != 'production';
return createConnection({
type: 'postgres',

View File

@@ -0,0 +1,4 @@
# AiScript
## 関数
デフォルトで値渡しです。

View File

@@ -27,6 +27,8 @@ APIを使い始めるには、まずアクセストークンを取得する必
UUIDを生成する。以後これをセッションIDと呼びます。
> このセッションIDは毎回生成し、使いまわさないようにしてください。
#### Step 2
`{_URL_}/miauth/{session}`をユーザーのブラウザで表示させる。`{session}`の部分は、セッションIDに置き換えてください。

74
src/docs/theme.ja-JP.md Normal file
View File

@@ -0,0 +1,74 @@
# テーマ
テーマを設定して、Misskeyクライアントの見た目を変更できます。
## テーマの設定
設定 > テーマ
## テーマを作成する
テーマコードはJSON5で記述されたテーマオブジェクトです。
テーマは以下のようなオブジェクトです。
``` js
{
id: '17587283-dd92-4a2c-a22c-be0637c9e22a',
name: 'Danboard',
author: 'syuilo',
base: 'light',
props: {
accent: 'rgb(218, 141, 49)',
bg: 'rgb(218, 212, 190)',
fg: 'rgb(115, 108, 92)',
panel: 'rgb(236, 232, 220)',
renote: 'rgb(100, 152, 106)',
link: 'rgb(100, 152, 106)',
mention: '@accent',
hashtag: 'rgb(100, 152, 106)',
header: 'rgba(239, 227, 213, 0.75)',
navBg: 'rgb(216, 206, 182)',
inputBorder: 'rgba(0, 0, 0, 0.1)',
},
}
```
* `id` ... テーマの一意なID。UUIDをおすすめします。
* `name` ... テーマ名
* `author` ... テーマの作者
* `desc` ... テーマの説明(オプション)
* `base` ... 明るいテーマか、暗いテーマか
* `light`にすると明るいテーマになり、`dark`にすると暗いテーマになります。
* テーマはここで設定されたベーステーマを継承します。
* `props` ... テーマのスタイル定義。これから説明します。
### テーマのスタイル定義
`props`下にはテーマのスタイルを定義します。
キーがCSSの変数名になり、バリューで中身を指定します。
なお、この`props`オブジェクトはベーステーマから継承されます。
ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。
つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。
#### バリューで使える構文
* 16進数で表された色
* 例: `#00ff00`
* `rgb(r, g, b)`形式で表された色
* 例: `rgb(0, 255, 0)`
* `rgb(r, g, b, a)`形式で表された透明度を含む色
* 例: `rgba(0, 255, 0, 0.5)`
* 他のキーの値の参照
* `@{キー名}`と書くと他のキーの値の参照になります。`{キー名}`は参照したいキーの名前に置き換えます。
* 例: `@panel`
* 定数(後述)の参照
* `${定数名}`と書くと定数の参照になります。`{定数名}`は参照したい定数の名前に置き換えます。
* 例: `$main`
* 関数(後述)
* `:{関数名}<{引数}<{色}`
#### 定数
「CSS変数として出力はしたくないが、他のCSS変数の値として使いまわしたい」値があるときは、定数を使うと便利です。
キー名を`$`で始めると、そのキーはCSS変数として出力されません。
#### 関数
wip

View File

@@ -226,10 +226,10 @@ export default class Reversi {
// 座標が指し示す位置がボード外に出たとき
if (this.opts.loopedBoard && this.transformXyToPos(
(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos)
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
return found;
else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight)
else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
return []; // 挟めないことが確定 (盤面外に到達)
const pos = this.transformXyToPos(x, y);

View File

@@ -1,7 +1,7 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { urlRegex } from './prelude';
export function fromHtml(html: string): string {
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
let text = '';
@@ -13,7 +13,7 @@ export function fromHtml(html: string): string {
return text.trim();
function getText(node: any): string {
if (node.nodeName == '#text') return node.value;
if (node.nodeName === '#text') return node.value;
if (node.childNodes) {
return node.childNodes.map((n: any) => getText(n)).join('');
@@ -34,29 +34,27 @@ export function fromHtml(html: string): string {
case 'a':
const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name == 'rel');
const href = node.attrs.find((x: any) => x.name == 'href');
const _class = node.attrs.find((x: any) => x.name == 'class');
const isHashtag = rel?.value?.match('tag') || _class?.value?.match('hashtag');
const rel = node.attrs.find((x: any) => x.name === 'rel');
const href = node.attrs.find((x: any) => x.name === 'href');
// ハッシュタグ / hrefがない / txtがURL
if (isHashtag || !href || href.value == txt) {
text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
// ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt;
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length == 2) {
if (part.length === 2) {
//#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
//#endregion
} else if (part.length == 3) {
} else if (part.length === 3) {
text += txt;
}
// その他
} else {
text += `[${txt}](${href.value})`;
text += (!href || (txt === href.value && txt.match(urlRegex))) ? txt : `[${txt}](${href.value})`;
}
break;

View File

@@ -31,7 +31,7 @@ export const mfmLanguage = P.createLanguage({
r.center,
),
startOfLine: () => P((input, i) => {
if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') {
return P.makeSuccess(i, null);
} else {
return P.makeFailure(i, 'not newline');
@@ -50,7 +50,7 @@ export const mfmLanguage = P.createLanguage({
if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote');
const quote = takeWhile(line => line.startsWith('>'), text.split('\n'));
const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, '');
if (qInner == '') return P.makeFailure(i, 'not a quote');
if (qInner === '') return P.makeFailure(i, 'not a quote');
const contents = r.root.tryParse(qInner);
return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {}));
})),

View File

@@ -4,7 +4,7 @@ import { MfmForest, MfmTree } from './prelude';
import { createTree, createLeaf } from '../prelude/tree';
function isEmptyTextTree(t: MfmTree): boolean {
return t.node.type == 'text' && t.node.props.text === '';
return t.node.type === 'text' && t.node.props.text === '';
}
function concatTextTrees(ts: MfmForest): MfmTree {

View File

@@ -3,7 +3,7 @@ import { MfmForest } from './prelude';
import { normalize } from './normalize';
export function parse(source: string | null): MfmForest | null {
if (source == null || source == '') {
if (source == null || source === '') {
return null;
}
@@ -11,7 +11,7 @@ export function parse(source: string | null): MfmForest | null {
}
export function parsePlain(source: string | null): MfmForest | null {
if (source == null || source == '') {
if (source == null || source === '') {
return null;
}

21
src/misc/secure-rndstr.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as crypto from 'crypto';
const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
export function secureRndstr(length = 32, useLU = true): string {
const chars = useLU ? LU_CHARS : L_CHARS;
const chars_len = chars.length;
let str = '';
for (let i = 0; i < length; i++) {
let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len);
if (rand === chars_len) {
rand = chars_len - 1;
}
str += chars.charAt(rand);
}
return str;
}

View File

@@ -112,6 +112,12 @@ export class Note {
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The human readable url of a note. it will be null when the note is local.'
})
public url: string | null;
@Column('integer', {
default: 0, select: false
})

View File

@@ -2,7 +2,6 @@ import { getRepository, getCustomRepository } from 'typeorm';
import { Announcement } from './entities/announcement';
import { AnnouncementRead } from './entities/announcement-read';
import { Instance } from './entities/instance';
import { Emoji } from './entities/emoji';
import { Poll } from './entities/poll';
import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
@@ -52,6 +51,7 @@ import { AntennaRepository } from './repositories/antenna';
import { AntennaNote } from './entities/antenna-note';
import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji';
export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead);
@@ -79,7 +79,7 @@ export const UsedUsernames = getRepository(UsedUsername);
export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository);
export const Instances = getRepository(Instance);
export const Emojis = getRepository(Emoji);
export const Emojis = getCustomRepository(EmojiRepository);
export const DriveFiles = getCustomRepository(DriveFileRepository);
export const DriveFolders = getCustomRepository(DriveFolderRepository);
export const Notifications = getCustomRepository(NotificationRepository);

View File

@@ -0,0 +1,27 @@
import { EntityRepository, Repository } from 'typeorm';
import { Emoji } from '../entities/emoji';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Emoji)
export class EmojiRepository extends Repository<Emoji> {
public async pack(
src: Emoji['id'] | Emoji,
) {
const emoji = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
url: emoji.url,
};
}
public packMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.pack(x)));
}
}

View File

@@ -39,7 +39,7 @@ export class NoteRepository extends Repository<Note> {
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility == 'followers') {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
@@ -171,8 +171,8 @@ export class NoteRepository extends Repository<Note> {
let text = note.text;
if (note.name && note.uri) {
text = `${note.name}\n${(note.text || '').trim()}\n${note.uri}`;
if (note.name && (note.url || note.uri)) {
text = `${note.name}\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
}
const packed = await awaitAll({
@@ -197,6 +197,7 @@ export class NoteRepository extends Repository<Note> {
renoteId: note.renoteId,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
url: note.url || undefined,
_featuredId_: (note as any)._featuredId_ || undefined,
_prId_: (note as any)._prId_ || undefined,

View File

@@ -46,8 +46,8 @@ export class NotificationRepository extends Repository<Notification> {
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader || token!.name,
icon: notification.customIcon || token!.iconUrl,
header: notification.customHeader || token?.name,
icon: notification.customIcon || token?.iconUrl,
} : {}),
});
}

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../type';
import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -8,7 +8,7 @@ import { Users } from '../../../../models';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
const uri = activity.id || activity;

View File

@@ -6,7 +6,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -0,0 +1,9 @@
import { IObject } from '../type';
import { extractApHashtagObjects } from '../models/tag';
import { fromHtml } from '../../../mfm/fromHtml';
export function htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
return fromHtml(html, hashtagNames);
}

View File

@@ -0,0 +1,24 @@
import { toArray, unique } from '../../../prelude/array';
import { IObject, isMention, IApMention } from '../type';
import { resolvePerson } from './person';
import * as promiseLimit from 'promise-limit';
import Resolver from '../resolver';
import { User } from '../../../models/entities/user';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const resolver = new Resolver();
const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
)).filter((x): x is User => x != null);
return mentionedUsers;
}
export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
if (tags == null) return [];
return toArray(tags).filter(isMention);
}

View File

@@ -6,9 +6,9 @@ import post from '../../../services/note/create';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml';
import { ITag, extractHashtags } from './tag';
import { unique } from '../../../prelude/array';
import { htmlToMfm } from '../misc/html-to-mfm';
import { extractApHashtags } from './tag';
import { unique, toArray, toSingle } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
@@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import { extractDbHost, toPuny } from '../../../misc/convert-host';
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, getOneApId, getApId, validPost, IPost } from '../type';
import { IObject, getOneApId, getApId, validPost, IPost, isEmoji } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id';
import { fetchMeta } from '../../../misc/fetch-meta';
@@ -25,6 +25,7 @@ import { ensure } from '../../../prelude/ensure';
import { getApLock } from '../../../misc/app-lock';
import { createMessage } from '../../../services/messages/create';
import { parseAudience } from '../audience';
import { extractApMentions } from './mention';
const logger = apLogger;
@@ -113,7 +114,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const noteAudience = await parseAudience(actor, note.to, note.cc);
let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers;
const apMentions = noteAudience.mentionedUsers;
// Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) {
@@ -125,7 +125,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
let isTalk = note._misskey_talk && visibility === 'specified';
const apHashtags = await extractHashtags(note.tag);
const apMentions = await extractApMentions(note.tag);
const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
@@ -210,7 +211,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
const text = note._misskey_content || (note.content ? fromHtml(note.content) : null);
const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null);
// vote
if (reply && reply.hasPoll) {
@@ -280,7 +281,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
apHashtags,
apEmojis,
poll,
uri: note.id
uri: note.id,
url: note.url,
}, silent);
}
@@ -291,7 +293,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value == 'string' ? value : value.id;
const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断
@@ -318,15 +320,16 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
}
}
export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> {
export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
host = toPuny(host);
if (!tags) return [];
const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name);
const eomjiTags = toArray(tags).filter(isEmoji);
return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
tag.icon = toSingle(tag.icon);
const exists = await Emojis.findOne({
host,

View File

@@ -3,12 +3,12 @@ import * as promiseLimit from 'promise-limit';
import config from '../../../config';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, IObject, isPropertyValue, IApPropertyValue } from '../type';
import { fromHtml } from '../../../mfm/fromHtml';
import { htmlToMfm } from '../misc/html-to-mfm';
import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import { ITag, extractHashtags } from './tag';
import { IIdentifier } from './identifier';
import { extractApHashtags } from './tag';
import { apLogger } from '../logger';
import { Note } from '../../../models/entities/note';
import { updateUsertags } from '../../../services/update-hashtag';
@@ -134,9 +134,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const isBot = object.type == 'Service';
const isBot = object.type === 'Service';
// Create user
let user: IRemoteUser;
@@ -165,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? fromHtml(person.summary) : null,
description: person.summary ? htmlToMfm(person.summary, person.tag) : null,
url: person.url,
fields,
userHost: host
@@ -180,11 +180,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
throw new Error('already registered');
}
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await Users.findOne({
uri: person.id
});
logger.error(e);
throw e;
if (u) {
user = u as IRemoteUser;
} else {
throw new Error('already registered');
}
} else {
logger.error(e);
throw e;
}
}
// Register host
@@ -308,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const updates = {
lastFetchedAt: new Date(),
@@ -318,7 +327,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
emojis: emojiNames,
name: person.name,
tags,
isBot: object.type == 'Service',
isBot: object.type === 'Service',
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
} as Partial<User>;
@@ -346,7 +355,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
await UserProfiles.update({ userId: exist.id }, {
url: person.url,
fields,
description: person.summary ? fromHtml(person.summary) : null,
description: person.summary ? htmlToMfm(person.summary, person.tag) : null,
});
// ハッシュタグ更新
@@ -384,16 +393,6 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<U
return await createPerson(uri, resolver);
}
const isPropertyValue = (x: {
type: string,
name?: string,
value?: string
}) =>
x &&
x.type === 'PropertyValue' &&
typeof x.name === 'string' &&
typeof x.value === 'string';
const services: {
[x: string]: (id: string, username: string) => any
} = {
@@ -409,7 +408,7 @@ const $discord = (id: string, name: string) => {
return { id, username, discriminator };
};
function addService(target: { [x: string]: any }, source: IIdentifier) {
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
const service = services[source.name];
if (typeof source.value !== 'string')
@@ -421,7 +420,7 @@ function addService(target: { [x: string]: any }, source: IIdentifier) {
target[source.name.split(':')[2]] = service(id, username);
}
export function analyzeAttachments(attachments: ITag[]) {
export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
const fields: {
name: string,
value: string
@@ -430,12 +429,12 @@ export function analyzeAttachments(attachments: ITag[]) {
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier!)) {
addService(services, attachment.identifier!);
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({
name: attachment.name!,
value: fromHtml(attachment.value!)
name: attachment.name,
value: fromHtml(attachment.value)
});
}
}

View File

@@ -41,7 +41,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @returns true if updated
*/
export async function updateQuestion(value: any) {
const uri = typeof value == 'string' ? value : value.id;
const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(config.url + '/')) throw new Error('uri points local');

View File

@@ -1,26 +1,18 @@
import { IIcon } from './icon';
import { IIdentifier } from './identifier';
import { toArray } from '../../../prelude/array';
import { IObject, isHashtag, IApHashtag } from '../type';
/***
* tag (ActivityPub)
*/
export type ITag = {
id: string;
type: string;
name?: string;
value?: string;
updated?: Date;
icon?: IIcon;
identifier?: IIdentifier;
};
export function extractHashtags(tags: ITag[] | null | undefined): string[] {
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return [];
const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string');
const hashtags = extractApHashtagObjects(tags);
return hashtags.map(tag => {
const m = tag.name ? tag.name.match(/^#(.+)/) : null;
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
}).filter(x => x != null) as string[];
}).filter((x): x is string => x != null);
}
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
if (tags == null) return [];
return toArray(tags).filter(isHashtag);
}

View File

@@ -7,10 +7,10 @@ export default (object: any, note: Note) => {
let to: string[] = [];
let cc: string[] = [];
if (note.visibility == 'public') {
if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`];
} else if (note.visibility == 'home') {
} else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'];
} else {

View File

@@ -4,6 +4,6 @@ import { Users } from '../../../models';
export default (mention: User) => ({
type: 'Mention',
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`,
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`,
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
});

View File

@@ -63,13 +63,13 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
let to: string[] = [];
let cc: string[] = [];
if (note.visibility == 'public') {
if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`].concat(mentions);
} else if (note.visibility == 'home') {
} else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
} else if (note.visibility == 'followers') {
} else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`];
cc = mentions;
} else {

View File

@@ -20,7 +20,8 @@ export interface IObject {
icon?: any;
image?: any;
url?: string;
tag?: any[];
href?: string;
tag?: IObject | IObject[];
sensitive?: boolean;
}
@@ -130,6 +131,45 @@ export const isOrderedCollection = (object: IObject): object is IOrderedCollecti
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
isCollection(object) || isOrderedCollection(object);
export interface IApPropertyValue extends IObject {
type: 'PropertyValue';
identifier: IApPropertyValue;
name: string;
value: string;
}
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object &&
object.type === 'PropertyValue' &&
typeof object.name === 'string' &&
typeof (object as any).value === 'string';
export interface IApMention extends IObject {
type: 'Mention';
href: string;
}
export const isMention = (object: IObject): object is IApMention=>
object.type === 'Mention' &&
typeof object.href === 'string';
export interface IApHashtag extends IObject {
type: 'Hashtag';
name: string;
}
export const isHashtag = (object: IObject): object is IApHashtag =>
object.type === 'Hashtag' &&
typeof object.name === 'string';
export interface IApEmoji extends IObject {
type: 'Emoji';
updated: Date;
}
export const isEmoji = (object: IObject): object is IApEmoji =>
object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
export interface ICreate extends IActivity {
type: 'Create';
}

View File

@@ -101,7 +101,7 @@ export default async (ctx: Router.RouterContext) => {
* @param note Note
*/
export async function packActivity(note: Note): Promise<any> {
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
const renote = await Notes.findOne(note.renoteId).then(ensure);
return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
}

View File

@@ -78,7 +78,7 @@ function verifyCertificateChain(certificates: string[]) {
}
function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
if (pemBuffer.length == 65 && pemBuffer[0] == 0x04) {
if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
type = 'PUBLIC KEY';
}

View File

@@ -34,7 +34,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => {
reply(res);
}).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind == 'client' ? 400 : 500, e);
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
});
}).catch(() => {
reply(403, new ApiError({

View File

@@ -77,7 +77,7 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
if (e instanceof ApiError) {
throw e;
} else {
apiLogger.error(`Internal error occurred in ${ep.name}`, {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, {
ep: ep.name,
ps: data,
e: {

View File

@@ -1,3 +1,3 @@
import rndstr from 'rndstr';
import { secureRndstr } from '../../../misc/secure-rndstr';
export default () => rndstr('a-zA-Z0-9', 16);
export default () => secureRndstr(16, true);

View File

@@ -22,8 +22,6 @@ export async function signup(username: User['username'], password: UserProfile['
throw new Error('INVALID_PASSWORD');
}
const usersCount = await Users.count({});
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
@@ -76,7 +74,9 @@ export async function signup(username: User['username'], password: UserProfile['
usernameLower: username.toLowerCase(),
host: toPunyNullable(host),
token: secret,
isAdmin: usersCount === 0,
isAdmin: (await Users.count({
host: null,
})) === 0,
}));
await transactionalEntityManager.save(new UserKeypair({

View File

@@ -15,12 +15,12 @@ type Params<T extends IEndpointMeta> = {
export type Response = Record<string, any> | void;
type executor<T extends IEndpointMeta> =
(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any, cleanup?: Function) =>
(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: Function) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any) => Promise<any> {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any) => {
: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}

View File

@@ -17,8 +17,10 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
const noUsers = (await Users.count({})) === 0;
if (!noUsers && me == null) throw new Error('access denied');
const noUsers = (await Users.count({
host: null,
})) === 0;
if (!noUsers && !me?.isAdmin) throw new Error('access denied');
const { account, secret } = await signup(ps.username, ps.password);

View File

@@ -51,8 +51,8 @@ const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
export default define(meta, async (ps, me) => {
const q = {} as any;
if (ps.origin == 'local') q['userHost'] = null;
if (ps.origin == 'remote') q['userHost'] = { $ne: null };
if (ps.origin === 'local') q['userHost'] = null;
if (ps.origin === 'remote') q['userHost'] = { $ne: null };
const files = await DriveFiles.find({
where: q,

View File

@@ -7,6 +7,7 @@ import { insertModerationLog } from '../../../../../services/insert-moderation-l
import { ApiError } from '../../../error';
import { ID } from '../../../../../misc/cafy-id';
import rndstr from 'rndstr';
import { publishBroadcastStream } from '../../../../../services/stream';
export const meta = {
desc: {
@@ -53,6 +54,10 @@ export default define(meta, async (ps, me) => {
await getConnection().queryResultCache!.remove(['meta_emojis']);
publishBroadcastStream('emojiAdded', {
emoji: await Emojis.pack(emoji.id)
});
insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id
});

View File

@@ -51,12 +51,5 @@ export default define(meta, async (ps) => {
.take(ps.limit!)
.getMany();
return emojis.map(e => ({
id: e.id,
name: e.name,
category: e.category,
aliases: e.aliases,
host: e.host,
url: e.url
}));
return Emojis.packMany(emojis);
});

View File

@@ -36,12 +36,5 @@ export default define(meta, async (ps) => {
.take(ps.limit!)
.getMany();
return emojis.map(e => ({
id: e.id,
name: e.name,
category: e.category,
aliases: e.aliases,
host: e.host,
url: e.url
}));
return Emojis.packMany(emojis);
});

View File

@@ -2,13 +2,14 @@ import define from '../../define';
import { getConnection } from 'typeorm';
export const meta = {
requireCredential: false as const,
requireCredential: true as const,
requireAdmin: true,
desc: {
'en-US': 'Get table stats'
},
tags: ['meta'],
tags: ['admin'],
params: {
},

View File

@@ -6,6 +6,8 @@ import { getNote } from '../../../common/getters';
import { PromoNotes } from '../../../../../models';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,

View File

@@ -5,12 +5,13 @@ import define from '../../define';
import redis from '../../../../db/redis';
export const meta = {
requireCredential: false as const,
requireCredential: true as const,
requireAdmin: true,
desc: {
},
tags: ['meta'],
tags: ['admin', 'meta'],
params: {
},

View File

@@ -5,6 +5,8 @@ import { Announcements, AnnouncementReads } from '../../../models';
import { makePaginationQuery } from '../common/make-pagination-query';
export const meta = {
tags: ['meta'],
requireCredential: false as const,
params: {

View File

@@ -8,7 +8,7 @@ import { generateMuteQuery } from '../../common/generate-mute-query';
import { ApiError } from '../../error';
export const meta = {
tags: ['account', 'notes', 'antennas'],
tags: ['antennas', 'account', 'notes'],
requireCredential: true as const,

View File

@@ -1,9 +1,9 @@
import rndstr from 'rndstr';
import $ from 'cafy';
import define from '../../define';
import { Apps } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
import { unique } from '../../../../prelude/array';
import { secureRndstr } from '../../../../misc/secure-rndstr';
export const meta = {
tags: ['app'],
@@ -60,7 +60,7 @@ export const meta = {
export default define(meta, async (ps, user) => {
// Generate secret
const secret = rndstr('a-zA-Z0-9', 32);
const secret = secureRndstr(32, true);
// for backward compatibility
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));

View File

@@ -1,4 +1,3 @@
import rndstr from 'rndstr';
import * as crypto from 'crypto';
import $ from 'cafy';
import define from '../../define';
@@ -6,6 +5,7 @@ import { ApiError } from '../../error';
import { AuthSessions, AccessTokens, Apps } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
import { ensure } from '../../../../prelude/ensure';
import { secureRndstr } from '../../../../misc/secure-rndstr';
export const meta = {
tags: ['auth'],
@@ -39,7 +39,7 @@ export default define(meta, async (ps, user) => {
}
// Generate access token
const accessToken = rndstr('a-zA-Z0-9', 32);
const accessToken = secureRndstr(32, true);
// Fetch exist access token
const exist = await AccessTokens.findOne({
@@ -60,6 +60,7 @@ export default define(meta, async (ps, user) => {
await AccessTokens.save({
id: genId(),
createdAt: new Date(),
lastUsedAt: new Date(),
appId: session.appId,
userId: user.id,
token: accessToken,

View File

@@ -13,7 +13,7 @@ export const meta = {
'en-US': 'Block a user.'
},
tags: ['blocking', 'users'],
tags: ['account'],
limit: {
duration: ms('1hour'),

View File

@@ -13,7 +13,7 @@ export const meta = {
'en-US': 'Unblock a user.'
},
tags: ['blocking', 'users'],
tags: ['account'],
limit: {
duration: ms('1hour'),

View File

@@ -10,7 +10,7 @@ export const meta = {
'en-US': 'Get blocking users.'
},
tags: ['blocking', 'account'],
tags: ['account'],
requireCredential: true as const,

View File

@@ -5,7 +5,7 @@ import { Followings } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
tags: ['users'],
tags: ['federation'],
requireCredential: false as const,

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