Compare commits

..

38 Commits

Author SHA1 Message Date
d4c7ca76ac 10.89.1 2019-02-24 13:30:05 +09:00
1a6aae944f 🎨 2019-02-24 13:29:08 +09:00
71e0241c94 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-02-24 12:58:55 +09:00
d838ef5b76 Resolve #4362 2019-02-24 12:58:45 +09:00
d90a5c9154 New Crowdin translations (#4350)
* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Catalan)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)
2019-02-24 12:54:57 +09:00
9b79a411e0 Use camelCase 2019-02-24 12:53:22 +09:00
b8e0ec9edc Improve doc 2019-02-24 12:40:17 +09:00
57009057ae Update typescript to 3.3.3333 🚀 2019-02-24 12:25:12 +09:00
5db7b2e193 Improve doc 2019-02-24 12:23:31 +09:00
d994c84901 Resolve audioTag undefined error (#4363) 2019-02-24 10:50:45 +09:00
febfb97bb4 Remember container state on user page (#4361)
* Remember container state on user page

* remove unnecessary code
2019-02-24 10:38:53 +09:00
a6c5e62923 assetsのjs要求時にsaltを付けないように (#4358)
* No assets salt

* remove related code
2019-02-24 10:38:00 +09:00
ac0390fec3 Improve doc 2019-02-24 10:08:09 +09:00
97b99867f2 Improve doc 2019-02-24 10:04:27 +09:00
a55d5516a6 Improve doc 2019-02-24 10:00:24 +09:00
b679163d01 Improve type definitions 2019-02-24 09:45:27 +09:00
76edcdbe45 Update schemas.ts 2019-02-24 04:27:09 +09:00
d8d51519c4 Update description.ts 2019-02-24 04:11:54 +09:00
3446969121 Refator: separate files 2019-02-24 04:08:08 +09:00
0e0c35a701 Fix bug 2019-02-24 03:56:15 +09:00
c9a6c9e20a テキストをまとめた 2019-02-24 03:54:04 +09:00
3d2b982a94 Use internal alert function 2019-02-24 03:52:12 +09:00
6157d8331b Update drive.vue 2019-02-24 03:50:24 +09:00
3d0fc09fae Implement mobile version folder deletion (#4355) 2019-02-24 03:49:07 +09:00
b975751710 Use global translation 2019-02-24 03:39:45 +09:00
4530d40537 Add missing translation - ホームのカスタマイズ (#4360) 2019-02-24 03:34:40 +09:00
94e3ac9b72 デッキからホームに行くメニューをホームと表記する (#4359) 2019-02-24 03:34:09 +09:00
e13fe97ebb Update update.ts (#4357)
* Update update.ts

* Update update.ts

* Update update.ts

* Update update.ts
2019-02-24 03:32:30 +09:00
4ad7632113 Fix bool-param-default 2019-02-23 17:21:59 +09:00
0709cac97f Fix indent 2019-02-23 16:23:15 +09:00
7dd4180fba Better error handling 2019-02-23 15:45:03 +09:00
558213490a Remove space 2019-02-23 15:42:52 +09:00
78a69e3da8 Update doc 2019-02-23 15:35:26 +09:00
140c78e5a7 Add operationId to more convinience 2019-02-23 14:59:37 +09:00
a8e18e0e22 Add source code link 2019-02-23 14:57:05 +09:00
2a8bb23625 Fix type 2019-02-23 14:56:48 +09:00
936a4d1bc4 Add reaction count (#4283) 2019-02-23 13:44:37 +09:00
69c34e8d00 [ImgBot] Optimize images (#4352)
/assets/api-doc.png -- 5.85kb -> 5.41kb (7.54%)
2019-02-23 13:34:35 +09:00
73 changed files with 1066 additions and 916 deletions

View File

@ -1,6 +1,14 @@
ChangeLog
=========
10.89.1
----------
* リアクション数を表示するように
* モバイル版でドライブのフォルダを削除できるように
* ドキュメントの強化
* プロフィールが更新できない場合がある問題を修正
* UIの修正
10.89.0
----------
* APIのエラーの形式を統一

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -58,6 +58,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Papierkorb"
drive: "Drive"
messaging: "Unterhaltungen"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -15,7 +15,7 @@ common:
reaction: "Reactions"
reaction-desc: "The easiest way to express your emotions. Misskey allows you to add various kinds of reactions to other's posts. The emotional experience on Misskey will never be on other SNSs, which are only able to push “likes”."
ui: "Interface"
ui-desc: "No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. Make your original home by editing, adjusting layouts of timeline and placing selectable widgets you can easily customize."
ui-desc: "No single UI can suit everyone. Therefore, Misskey has a highly customizable UI for your tastes. You can make your home original by editing the layout of your timeline, and moving around selectable widgets that you can easily adjust to make this place your own."
drive: "Drive"
drive-desc: "Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online."
outro: "Check Misskey-unique features by seeing them with your own eyes! If you feel like this instance is not for you, try other instances, as Misskey is a decentralized SNS, so that you can easily find your mates. Then, GLHF!"
@ -28,7 +28,7 @@ common:
load-more: "Load more"
enter-password: "Please enter the Password"
2fa: "Two-factor authentication"
customize-home: "Customize your home layout"
customize-home: "Customize home layout"
featured-notes: "Featured notes"
got-it: "Got it!"
customization-tips:
@ -58,6 +58,7 @@ common:
trash: "Trash"
drive: "Drive"
messaging: "Talk"
home: "Home"
deck: "Deck"
timeline: "Timeline"
explore: "Explore"
@ -577,7 +578,7 @@ common/views/widgets/memo.vue:
memo: "Write here!"
save: "Save"
common/views/widgets/slideshow.vue:
folder-customize-mode: "To specify a folder, please exit customize mode"
folder-customize-mode: "To specify a folder, please exit customization mode"
folder: "Please click and specify a folder"
no-image: "There is no image in this folder"
common/views/widgets/tips.vue:
@ -588,7 +589,7 @@ common/views/widgets/tips.vue:
tips-line5: "You can upload files by dragging and dropping them to Drive."
tips-line6: "You can move a folder by dragging it within the Drive."
tips-line7: "You can move folders by dragging them within the Drive."
tips-line8: "Home can be customized from the settings."
tips-line8: "The Home layout can be customized from the settings."
tips-line9: "Misskey is licensed under AGPLv3."
tips-line10: "Using the Time Machine widget makes it easy to trace back to the past timeline."
tips-line11: "You can pin posts to user page by clicking on \"...\""
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "What do you want to do? (Please enter a number): <1 → Upload a file | 2 → Upload a file from a URL | 3 → Create a folder | 4 → Change this folder's name | 5 → Move this folder | 6 → Delete this folder>"
deletion-alert: "Sorry! Deleting a folder is not yet implemented."
folder-name: "Folder name"
root-rename-alert: "You're in the root; it can't be renamed because it's not a folder. Navigate to a folder you want to rename and try again."
root-move-alert: "You're in the root; it can't be moved because it's not a folder. Navigate to a folder you want to move and try again."
here-is-root: "Currently, you are on the root, not inside of any folder."
url-prompt: "URL of the file you want to upload"
uploading: "Upload requested. It may take a while for the upload to finish."
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Papelera"
drive: "Drive"
messaging: "Conversación"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Corbeille"
drive: "Drive"
messaging: "Conversations"
home: "ホーム"
deck: "Deck"
timeline: "Fil"
explore: "Découvrir"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "Que veux-tu faire ? (Entrez un nombre): <1 → Télécharger le fichier | 2 → Télécharger le fichier avec l'URL | 3 → Créer le dossier | 4 → Modifier le nom du dossier | 5 → Déplacer ce dossier | 6 → Supprimer ce dossier >"
deletion-alert: "Désolé ! La suppression dun dossier nest pas encore implémentée."
folder-name: "Nom du dossier"
root-rename-alert: "L'emplacement actuel est la racine, pas le dossier, vous ne pouvez donc pas le renommer. Veuillez vous déplacer dans le dossier dont vous souhaitez modifier le nom."
root-move-alert: "L'emplacement actuel est la racine, ce n'est pas un dossier et il ne peut pas être déplacé. Veuillez vous déplacer dans le dossier que vous souhaitez déplacer."
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "URL du fichier que vous souhaitez téléverser"
uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever."
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -60,6 +60,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -155,6 +156,8 @@ common:
view-on-remote: "正確な情報を見る"
renoted-by: "{user}がRenote"
no-notes: "投稿がありません"
turn-on-darkmode: "闇に飲まれる"
turn-off-darkmode: "光あれ"
error:
title: "問題が発生しました"
@ -1561,8 +1564,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"

View File

@ -58,6 +58,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何すんの?(数字を入れてや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!"
folder-name: "フォルダー名"
root-rename-alert: "現在る場所はルートで、フォルダとちゃうから名前の変更はできへん。名前を変更したいフォルダに移動してからやってな。"
root-move-alert: "現在おる場所はルートで、フォルダとちゃうから移動はできへん。移動したいフォルダに移動してからやってな。"
here-is-root: "現在る場所はルートで、フォルダではありません。"
url-prompt: "このURLのファイルをアップロードしたいねん"
uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "휴지통"
drive: "드라이브"
messaging: "대화"
home: "ホーム"
deck: "덱"
timeline: "타임라인"
explore: "발견"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "무엇을 하시겠습니까? (숫자를 입력하여 주십시오): <1 → 파일 업로드 | 2 → 파일을 URL에서 업로드 | 3 → 폴더 만들기 | 4 → 이 폴더의 이름을 변경 | 5 → 현재 폴더 이동| 6 → 현재 폴더 삭제>"
deletion-alert: "죄송합니다! 폴더 삭제는 아직 구현되지 않았습니다..."
folder-name: "폴더 이름"
root-rename-alert: "현재 위치가 루트이고, 폴더가 아니므로 이름을 변경할 수 없습니다. 이름을 바꾸고 싶은 폴더로 이동하여 주십시오."
root-move-alert: "현재 위치가 루트이므로, 폴더가 아니므로 이동할 수 없습니다. 이동하고 싶은 폴더로 이동하여 주십시오."
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "업로드 하려는 파일의 URL"
uploading: "업로드를 요청하였습니다. 업로드가 완료될 때까지 시간이 소요될 수 있습니다."
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Papirkurv"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -9,10 +9,10 @@ common:
intro:
title: "Czym jest Misskey?"
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
features: "特徴"
features: "Funkcje"
rich-contents: "Wpis"
rich-contents-desc: "Po prostu opublikuj swój pomysł, gorące tematy i wszystko, co chcesz udostępnić. Możesz ozdobić swoje słowa, dołączyć swoje ulubione zdjęcia, wysłać pliki, w tym filmy i utworzyć ankietę - to są rzeczy, które możesz zrobić w Misskey!"
reaction: "Reakcje"
reaction: "Reakcja"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
ui: "Interfejs"
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
@ -22,7 +22,7 @@ common:
adblock:
detected: "Spróbuj wyłączyć blokadę reklam."
warning: "<strong>Misskey nie zawiera reklam</strong>, ale część funkcji może nie działać prawidłowo z włączonym blokowaniem reklam."
application-authorization: "アプリの連携"
application-authorization: "Współpraca aplikacji"
close: "Zamknij"
do-not-copy-paste: "ここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。"
load-more: "Załaduj więcej"
@ -58,9 +58,10 @@ common:
trash: "Kosz"
drive: "Dysk"
messaging: "Rozmowy"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
explore: "Znajdź"
following: "フォロー中"
followers: "フォロワー"
empty-timeline-info:
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "Co chcesz zrobić? (wprowadź odpowiednią cyfrę): <1 → Wysłać plik | 2 → Wysłać plik z adresu URL | 3 → Utworzyć katalog | 4 → Zmienić nazwę tego katalogu | 5 → Przenieść ten katalog | 6 → Usunąć ten katalog>"
deletion-alert: "Przepraszamy. Usuwanie katalogów nie zostało jeszcze zaimplementowane."
folder-name: "Nazwa katalogu"
root-rename-alert: "Nie można zmienić nazwy katalogu głównego. Przejdź do katalogu, którego nazwę chcesz zmienić."
root-move-alert: "Nie można przenieść tego katalogu, ponieważ jest on katalogiem głównym. Przejdź do katalogu, który chcesz przenieść."
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "Adres URL pliku, który chcesz wysłać"
uploading: "Rozpoczęto wysyłanie. Może to trochę potrwać."
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Lixo"
drive: "ドライブ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "Мусорное ведро"
drive: "Drive"
messaging: "Чат"
home: "ホーム"
deck: "デッキ"
timeline: "タイムライン"
explore: "みつける"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
folder-name: "フォルダー名"
root-rename-alert: "現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。"
root-move-alert: "現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "アップロードしたいファイルのURL"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -58,6 +58,7 @@ common:
trash: "垃圾箱"
drive: "网盘"
messaging: "聊天"
home: "ホーム"
deck: "Deck"
timeline: "时间线"
explore: "发现"
@ -1400,8 +1401,7 @@ mobile/views/components/drive.vue:
prompt: "您想要干什么呢?(请输入数字):<1 → 上传文件 | 2 → 从URL上传文件 | 3 → 创建新文件夹 | 4 → 更改这个文件夹的名称 | 5 → 移动这个文件夹 | 6 → 删除这个文件夹>"
deletion-alert: "抱歉! 删除文件夹功能尚未实现。"
folder-name: "文件夹名称"
root-rename-alert: "您目前在root模式; 它无法重命名,因为它不是文件夹。 导航到要重命名的文件夹,然后重试。"
root-move-alert: "您目前在root模式; 它无法移动,因为它不是文件夹。 导航到要移动的文件夹,然后重试。"
here-is-root: "現在いる場所はルートで、フォルダではありません。"
url-prompt: "要上传的文件的URL"
uploading: "已请求上传。 上传完成可能需要一段时间。"
mobile/views/components/drive-file-chooser.vue:

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.89.0",
"version": "10.89.1",
"codename": "nighthike",
"repository": {
"type": "git",
@ -221,7 +221,7 @@
"ts-node": "7.0.1",
"tslint": "5.12.1",
"tslint-sonarts": "1.9.0",
"typescript": "3.2.4",
"typescript": "3.3.3333",
"typescript-eslint-parser": "21.0.2",
"uglify-es": "3.3.9",
"url-loader": "1.1.2",

View File

@ -26,14 +26,14 @@ declare module 'webfinger.js' {
}
interface IIDXLinks {
'avatar': IJRDLink[];
'remotestorage': IJRDLink[];
'blog': IJRDLink[];
'vcard': IJRDLink[];
'updates': IJRDLink[];
'share': IJRDLink[];
'profile': IJRDLink[];
'webfist': IJRDLink[];
'avatar': IJRDLink[];
'remotestorage': IJRDLink[];
'blog': IJRDLink[];
'vcard': IJRDLink[];
'updates': IJRDLink[];
'share': IJRDLink[];
'profile': IJRDLink[];
'webfist': IJRDLink[];
'camlistore': IJRDLink[];
[type: string]: IJRDLink[];
}

View File

@ -108,16 +108,11 @@
app = isMobile ? 'mobile' : 'desktop';
}
// Get salt query
const salt = localStorage.getItem('salt')
? `?salt=${localStorage.getItem('salt')}`
: '';
// Load an app script
// Note: 'async' make it possible to load the script asyncly.
// 'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${ver}.js${salt}`);
script.setAttribute('src', `/assets/${app}.${ver}.js`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
@ -155,9 +150,6 @@
localStorage.removeItem('locale');
// Random
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
// Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');

View File

@ -44,7 +44,7 @@ export default Vue.extend({
},
mounted() {
const audioTag = this.$refs.audio as HTMLAudioElement;
audioTag.volume = this.$store.state.device.mediaVolume;
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
},
methods: {
volumechange() {

View File

@ -10,13 +10,15 @@
<span>{{ $t('username') }}</span>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #desc v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</template>
<template #desc v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</template>
<template #desc v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</template>
<template #desc v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</template>
<template #desc v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</template>
<template #desc v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</template>
<template #desc v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</template>
<template #desc>
<span v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</span>
<span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</span>
<span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</span>
<span v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</span>
<span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</span>
<span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</span>
<span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</span>
</template>
</ui-input>
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
<span>{{ $t('password') }}</span>

View File

@ -22,7 +22,7 @@ export default define({
name: 'rss',
props: () => ({
compact: false,
url: 'http://news.yahoo.co.jp/pickup/rss.xml'
url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
})
}).extend({
i18n: i18n(),
@ -78,6 +78,9 @@ export default define({
padding 4px 0
color var(--text)
border-bottom dashed var(--lineWidth) var(--faceDivider)
white-space nowrap
text-overflow ellipsis
overflow hidden
&:last-child
border-bottom none

View File

@ -48,16 +48,19 @@
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button>
<button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton" @click="renote()" :title="$t('renote')">
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
<fa icon="retweet"/>
<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="inhibitedButton">
<fa icon="ban"/>
</button>
<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton" :title="$t('add-reaction')">
<fa icon="plus"/>
<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
</button>
<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')">
<fa icon="minus"/>
<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
</button>
<button @click="menu()" ref="menuButton">
<fa icon="ellipsis-h"/>

View File

@ -71,7 +71,7 @@
<section>
<ui-switch v-model="showPostFormOnTopOfTl">{{ $t('post-form-on-timeline') }}</ui-switch>
<ui-button @click="customizeHome">{{ $t('customize') }}</ui-button>
<ui-button @click="customizeHome">{{ $t('@.customize-home') }}</ui-button>
</section>
<section>
<header>{{ $t('wallpaper') }}</header>

View File

@ -3,7 +3,7 @@
<header v-if="showHeader">
<div class="title"><slot name="header"></slot></div>
<slot name="func"></slot>
<button v-if="bodyTogglable" @click="() => showBody = !showBody">
<button v-if="bodyTogglable" @click="toggleContent(!showBody)">
<template v-if="showBody"><fa icon="angle-up"/></template>
<template v-else><fa icon="angle-down"/></template>
</button>
@ -48,6 +48,7 @@ export default Vue.extend({
methods: {
toggleContent(show: boolean) {
this.showBody = show;
this.$emit('toggle', show);
}
}
});

View File

@ -62,16 +62,14 @@
<ul>
<li @click="toggleDeckMode">
<p>
<span>{{ $t('@.deck') }}</span>
<template v-if="$store.state.device.inDeckMode"><i><fa :icon="faHome"/></i></template>
<template v-else><i><fa :icon="faColumns"/></i></template>
<template v-if="$store.state.device.inDeckMode"><span>{{ $t('@.home') }}</span><i><fa :icon="faHome"/></i></template>
<template v-else><span>{{ $t('@.deck') }}</span><i><fa :icon="faColumns"/></i></template>
</p>
</li>
<li @click="dark">
<p>
<span>{{ $t('dark') }}</span>
<template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template>
<template v-else><i><fa :icon="['far', 'moon']"/></i></template>
<span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span>
<template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon"/></i></template>
</p>
</li>
</ul>
@ -98,13 +96,14 @@ import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('desktop/views/components/ui.header.account.vue'),
data() {
return {
isOpen: false,
faHome, faColumns
faHome, faColumns, faMoon, faSun
};
},
computed: {

View File

@ -6,7 +6,9 @@
<x-note v-for="n in user.pinnedNotes" :key="n.id" :note="n" :mini="true"/>
</div>
</ui-container>
<ui-container v-if="images.length > 0" :body-togglable="true">
<ui-container v-if="images.length > 0" :body-togglable="true"
:expanded="$store.state.device.expandUsersPhotos"
@toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })">
<template #header><fa :icon="['far', 'images']"/> {{ $t('images') }}</template>
<div class="sainvnaq">
<router-link v-for="image in images"
@ -17,7 +19,9 @@
></router-link>
</div>
</ui-container>
<ui-container :body-togglable="true">
<ui-container :body-togglable="true"
:expanded="$store.state.device.expandUsersActivity"
@toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })">
<template #header><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</template>
<div>
<div ref="chart"></div>

View File

@ -3,7 +3,9 @@
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
<div class="activity">
<ui-container :body-togglable="true">
<ui-container :body-togglable="true"
:expanded="$store.state.device.expandUsersActivity"
@toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })">
<template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template>
<x-activity :user="user" :limit="35" style="padding: 16px;"/>
</ui-container>

View File

@ -1,5 +1,7 @@
<template>
<ui-container :body-togglable="true">
<ui-container :body-togglable="true"
:expanded="$store.state.device.expandUsersPhotos"
@toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })">
<template #header><fa icon="camera"/> {{ $t('title') }}</template>
<div class="dzsuvbsrrrwobdxifudxuefculdfiaxd">

View File

@ -399,7 +399,7 @@ export default Vue.extend({
this.moveFolder();
break;
case '6':
alert(this.$t('deletion-alert'));
this.deleteFolder();
break;
}
},
@ -421,7 +421,10 @@ export default Vue.extend({
renameFolder() {
if (this.folder == null) {
alert(this.$t('root-rename-alert'));
this.$root.dialog({
type: 'error',
text: this.$t('here-is-root')
});
return;
}
const name = window.prompt(this.$t('folder-name'), this.folder.name);
@ -436,7 +439,10 @@ export default Vue.extend({
moveFolder() {
if (this.folder == null) {
alert(this.$t('root-move-alert'));
this.$root.dialog({
type: 'error',
text: this.$t('here-is-root')
});
return;
}
this.$chooseDriveFolder().then(folder => {
@ -456,13 +462,31 @@ export default Vue.extend({
url: url,
folderId: this.folder ? this.folder.id : undefined
});
alert(this.$t('uploading'));
this.$root.dialog({
type: 'info',
text: this.$t('uploading')
});
},
onChangeLocalFile() {
for (const f of Array.from((this.$refs.file as any).files)) {
(this.$refs.uploader as any).upload(f, this.folder);
}
},
deleteFolder() {
if (this.folder == null) {
this.$root.dialog({
type: 'error',
text: this.$t('here-is-root')
});
return;
}
this.$root.api('drive/folders/delete', {
folderId: this.folder.id
}).then(folder => {
this.cd(this.folder.parentId);
});
}
}
});

View File

@ -33,7 +33,7 @@
<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li>
<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon" fixed-width/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
<li @click="dark"><p><template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i></template><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li>
</ul>
</div>
<div class="announcements" v-if="announcements && announcements.length > 0">
@ -53,6 +53,7 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import { lang } from '../../../config';
import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('mobile/views/components/ui.nav.vue'),
@ -65,7 +66,7 @@ export default Vue.extend({
aboutUrl: `/docs/${lang}/about`,
announcements: [],
searching: false,
faNewspaper, faHashtag
faNewspaper, faHashtag, faMoon, faSun
};
},

View File

@ -68,7 +68,9 @@ const defaultDeviceSettings = {
mobileNotificationPosition: 'bottom',
deckMode: false,
useOsDefaultEmojis: false,
disableShowingAnimatedImages: false
disableShowingAnimatedImages: false,
expandUsersPhotos: true,
expandUsersActivity: true,
};
export default (os: MiOS) => new Vuex.Store({

View File

@ -32,16 +32,12 @@ export default function load() {
mixin.host = url.host;
mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, '');
mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
mixin.user_agent = `Misskey/${pkg.version} (${config.url})`;
mixin.wsScheme = mixin.scheme.replace('http', 'ws');
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${pkg.version} (${config.url})`;
if (config.autoAdmin == null) config.autoAdmin = false;

View File

@ -49,16 +49,12 @@ export type Mixin = {
host: string;
hostname: string;
scheme: string;
ws_scheme: string;
api_url: string;
ws_url: string;
auth_url: string;
docs_url: string;
stats_url: string;
status_url: string;
dev_url: string;
drive_url: string;
user_agent: string;
wsScheme: string;
apiUrl: string;
wsUrl: string;
authUrl: string;
driveUrl: string;
userAgent: string;
};
export type Config = Source & Mixin;

View File

@ -1,80 +1,3 @@
# Misskey API
MisskeyのWeb APIを使って、プログラムからMisskeyの様々な機能にアクセスすることができます。
APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。
## 自分の所有するアカウントからAPIにアクセスする場合
「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
<div class="ui info warn">
<p><i class="fas fa-exclamation-triangle"></i> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
</div>
APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
## アプリケーションからAPIにアクセスする場合
直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
そのトークンをリクエストのパラメータに含める必要があります。
<div class="ui info">
<p><i class="fas fa-info-circle"></i> アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます</p>
</div>
### 1.アプリケーションを登録する
まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。
フォームの記入欄の説明は以下の通りです:
| 名前 | 説明 |
|---|---|
| アプリケーション名 | あなたのアプリの名称。 |
| アプリの概要 | あなたのアプリの簡単な説明や紹介。 |
| コールバックURL | ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 |
| 権限 | あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 |
登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
<div class="ui info warn">
<p><i class="fas fa-exclamation-triangle"></i> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p>
</div>
### 2.ユーザーに認証させる
アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
認証セッションを開始するには、%API_URL%/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。
リクエスト形式はJSONで、メソッドはPOSTです。
レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
あなたのアプリがコールバックURLを設定している場合、
ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
### 3.ユーザーのアクセストークンを取得する
ユーザーが連携を許可したら、%API_URL%/auth/session/userkey へ次のパラメータを含むリクエストを送信します:
| 名前 | 型 | 説明 |
|---|---|---|
| appSecret | string | アプリのシークレットキー |
| token | string | セッションのトークン |
上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
「i」パラメータの生成方法を擬似コードで表すと次のようになります:
<pre><code>const i = sha256(accessToken + secretKey);</code></pre>
APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
## Misskey APIの利用
APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
ストリーミングAPIも提供しています。
[APIリファレンス](/api-doc)もご確認ください。
### レートリミット
Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。
[APIリファレンス](/api-doc)

View File

@ -14,9 +14,9 @@ export default function(file: IDriveFile, thumbnail = false): string {
}
} else {
if (thumbnail) {
return `${config.drive_url}/${file._id}?thumbnail`;
return `${config.driveUrl}/${file._id}?thumbnail`;
} else {
return `${config.drive_url}/${file._id}?web`;
return `${config.driveUrl}/${file._id}?web`;
}
}
}
@ -27,5 +27,5 @@ export function getOriginalUrl(file: IDriveFile) {
}
const accessKey = file.metadata ? file.metadata.accessKey : null;
return `${config.drive_url}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
return `${config.driveUrl}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
}

View File

@ -76,8 +76,8 @@ export const pack = (
}
_app.iconUrl = _app.icon != null
? `${config.drive_url}/${_app.icon}`
: `${config.drive_url}/app-default.jpg`;
? `${config.driveUrl}/${_app.icon}`
: `${config.driveUrl}/app-default.jpg`;
if (me) {
// 既に連携しているか

View File

@ -172,7 +172,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
!isLocalUser(user);
//#region Validators
export function validateUsername(username: string, remote?: boolean): boolean {
export function validateUsername(username: string, remote = false): boolean {
return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
}
@ -350,7 +350,7 @@ export const pack = (
}
if (_user.avatarUrl == null) {
_user.avatarUrl = `${config.drive_url}/default-avatar.jpg`;
_user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`;
}
if (!meId || !meId.equals(_user.id) || !opts.detail) {

38
src/prelude/schema.ts Normal file
View File

@ -0,0 +1,38 @@
export type Schema = {
type: 'number' | 'string' | 'array' | 'object' | any;
optional?: boolean;
items?: Schema;
properties?: Obj;
description?: string;
};
export type Obj = { [key: string]: Schema };
export type ObjType<s extends Obj> = { [P in keyof s]: SchemaType<s[P]> };
// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
type MyType<T extends Schema> = {
0: any;
1: SchemaType<T>;
}[T extends Schema ? 1 : 0];
export type SchemaType<p extends Schema> =
p['type'] extends 'number' ? number :
p['type'] extends 'string' ? string :
p['type'] extends 'array' ? MyType<p['items']>[] :
p['type'] extends 'object' ? ObjType<p['properties']> :
any;
export function convertOpenApiSchema(schema: Schema) {
const x = JSON.parse(JSON.stringify(schema)); // copy
if (!['string', 'number', 'boolean', 'array', 'object'].includes(x.type)) {
x['$ref'] = `#/components/schemas/${x.type}`;
}
if (x.type === 'object' && x.properties) {
x.required = Object.entries(x.properties).filter(([k, v]: any) => !v.isOptional).map(([k, v]: any) => k);
for (const k of Object.keys(x.properties)) {
x.properties[k] = convertOpenApiSchema(x.properties[k]);
}
}
return x;
}

View File

@ -45,7 +45,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise(async
timeout,
headers: {
'Host': host,
'User-Agent': config.user_agent,
'User-Agent': config.userAgent,
'Content-Type': 'application/activity+json',
'Digest': `SHA-256=${hash}`
}

View File

@ -60,7 +60,7 @@ export default class Resolver {
proxy: config.proxy,
timeout: this.timeout,
headers: {
'User-Agent': config.user_agent,
'User-Agent': config.userAgent,
Accept: 'application/activity+json, application/ld+json'
},
json: true

View File

@ -13,7 +13,15 @@ export default (endpoint: IEndpoint, ctx: Koa.BaseContext) => new Promise((res)
ctx.status = 204;
} else if (typeof x === 'number') {
ctx.status = x;
ctx.body = { error: y };
ctx.body = {
error: {
message: y.message,
code: y.code,
id: y.id,
kind: y.kind,
...(y.info ? { info: y.info } : {})
}
};
} else {
ctx.body = x;
}
@ -26,7 +34,7 @@ export default (endpoint: IEndpoint, ctx: Koa.BaseContext) => new Promise((res)
call(endpoint.name, user, app, body, (ctx.req as any).file).then(res => {
reply(res);
}).catch(e => {
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

@ -1,6 +1,7 @@
import { Context } from 'cafy';
import * as path from 'path';
import * as glob from 'glob';
import { Schema } from '../../prelude/schema';
export type Param = {
validator: Context<any>;
@ -29,7 +30,7 @@ export interface IEndpointMeta {
};
};
res?: any;
res?: Schema;
/**
* このエンドポイントにリクエストするのにユーザー情報が必須か否か

View File

@ -13,7 +13,11 @@ export const meta = {
params: {
appSecret: {
validator: $.str
validator: $.str,
desc: {
'ja-JP': 'アプリケーションのシークレットキー',
'en-US': 'The secret key of your application.'
}
}
},
@ -48,6 +52,6 @@ export default define(meta, async (ps) => {
return {
token: doc.token,
url: `${config.auth_url}/${doc.token}`
url: `${config.authUrl}/${doc.token}`
};
});

View File

@ -10,7 +10,11 @@ export const meta = {
params: {
token: {
validator: $.str
validator: $.str,
desc: {
'ja-JP': 'セッションのトークン',
'en-US': 'The token of a session.'
}
}
},

View File

@ -13,11 +13,19 @@ export const meta = {
params: {
appSecret: {
validator: $.str
validator: $.str,
desc: {
'ja-JP': 'アプリケーションのシークレットキー',
'en-US': 'The secret key of your application.'
}
},
token: {
validator: $.str
validator: $.str,
desc: {
'ja-JP': 'セッションのトークン',
'en-US': 'The token of a session.'
}
}
},

View File

@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
import driveChart from '../../../../services/chart/drive';
import driveChart, { driveLogSchema } from '../../../../services/chart/drive';
import { convertLog } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@ -28,12 +29,7 @@ export const meta = {
},
},
res: {
type: 'array',
items: {
type: 'object',
},
},
res: convertLog(driveLogSchema),
};
export default define(meta, async (ps) => {

View File

@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
import notesChart from '../../../../services/chart/notes';
import notesChart, { notesLogSchema } from '../../../../services/chart/notes';
import { convertLog } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@ -28,12 +29,7 @@ export const meta = {
},
},
res: {
type: 'array',
items: {
type: 'object',
},
},
res: convertLog(notesLogSchema),
};
export default define(meta, async (ps) => {

View File

@ -1,7 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
import perUserDriveChart from '../../../../../services/chart/per-user-drive';
import perUserDriveChart, { perUserDriveLogSchema } from '../../../../../services/chart/per-user-drive';
import ID, { transform } from '../../../../../misc/cafy-id';
import { convertLog } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@ -38,12 +39,7 @@ export const meta = {
}
},
res: {
type: 'array',
items: {
type: 'object',
},
},
res: convertLog(perUserDriveLogSchema),
};
export default define(meta, async (ps) => {

View File

@ -1,7 +1,8 @@
import $ from 'cafy';
import define from '../../../define';
import perUserNotesChart from '../../../../../services/chart/per-user-notes';
import perUserNotesChart, { perUserNotesLogSchema } from '../../../../../services/chart/per-user-notes';
import ID, { transform } from '../../../../../misc/cafy-id';
import { convertLog } from '../../../../../services/chart';
export const meta = {
stability: 'stable',
@ -38,12 +39,7 @@ export const meta = {
}
},
res: {
type: 'array',
items: {
type: 'object',
},
},
res: convertLog(perUserNotesLogSchema),
};
export default define(meta, async (ps) => {

View File

@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
import usersChart from '../../../../services/chart/users';
import usersChart, { usersLogSchema } from '../../../../services/chart/users';
import { convertLog } from '../../../../services/chart';
export const meta = {
stability: 'stable',
@ -28,12 +29,7 @@ export const meta = {
},
},
res: {
type: 'array',
items: {
type: 'object',
},
},
res: convertLog(usersLogSchema),
};
export default define(meta, async (ps) => {

View File

@ -73,12 +73,12 @@ export default define(meta, async (ps, user) => {
'metadata.deletedAt': { $exists: false }
});
} else if (ps.url) {
const isInternalStorageUrl = ps.url.startsWith(config.drive_url);
const isInternalStorageUrl = ps.url.startsWith(config.driveUrl);
if (isInternalStorageUrl) {
// Extract file ID from url
// e.g.
// http://misskey.local/files/foo?original=bar --> foo
const fileId = new mongo.ObjectID(ps.url.replace(config.drive_url, '').replace(/\?(.*)$/, '').replace(/\//g, ''));
const fileId = new mongo.ObjectID(ps.url.replace(config.driveUrl, '').replace(/\?(.*)$/, '').replace(/\//g, ''));
file = await DriveFile.findOne({
_id: fileId,
'metadata.deletedAt': { $exists: false }

View File

@ -15,8 +15,7 @@ export const meta = {
params: {},
res: {
type: 'entity',
entity: 'User'
type: 'User',
}
};

View File

@ -159,7 +159,7 @@ export const meta = {
message: 'The file specified as a banner is not an image.',
code: 'BANNER_NOT_AN_IMAGE',
id: '75aedb19-2afd-4e6d-87fc-67941256fa60'
},
}
}
};
@ -192,10 +192,14 @@ export default define(meta, async (ps, user, app) => {
if (avatar == null) throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.contentType.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
updates.avatarUrl = getDriveFileUrl(avatar, true);
if (avatar.metadata.deletedAt) {
updates.avatarUrl = null;
} else {
updates.avatarUrl = getDriveFileUrl(avatar, true);
if (avatar.metadata.properties.avgColor) {
updates.avatarColor = avatar.metadata.properties.avgColor;
if (avatar.metadata.properties.avgColor) {
updates.avatarColor = avatar.metadata.properties.avgColor;
}
}
}
@ -207,10 +211,14 @@ export default define(meta, async (ps, user, app) => {
if (banner == null) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.contentType.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
updates.bannerUrl = getDriveFileUrl(banner, false);
if (banner.metadata.deletedAt) {
updates.bannerUrl = null;
} else {
updates.bannerUrl = getDriveFileUrl(banner, false);
if (banner.metadata.properties.avgColor) {
updates.bannerColor = banner.metadata.properties.avgColor;
if (banner.metadata.properties.avgColor) {
updates.bannerColor = banner.metadata.properties.avgColor;
}
}
}
@ -225,10 +233,14 @@ export default define(meta, async (ps, user, app) => {
if (wallpaper == null) throw new Error('wallpaper not found');
updates.wallpaperUrl = getDriveFileUrl(wallpaper);
if (wallpaper.metadata.deletedAt) {
updates.wallpaperUrl = null;
} else {
updates.wallpaperUrl = getDriveFileUrl(wallpaper);
if (wallpaper.metadata.properties.avgColor) {
updates.wallpaperColor = wallpaper.metadata.properties.avgColor;
if (wallpaper.metadata.properties.avgColor) {
updates.wallpaperColor = wallpaper.metadata.properties.avgColor;
}
}
}
}

View File

@ -175,12 +175,10 @@ export const meta = {
res: {
type: 'object',
props: {
properties: {
createdNote: {
type: 'Note',
desc: {
'ja-JP': '作成した投稿'
}
description: '作成した投稿'
}
}
},

View File

@ -1,507 +0,0 @@
import endpoints from './endpoints';
import { Context } from 'cafy';
import config from '../../config';
const basicErrors = {
'400': {
'INVALID_PARAM': {
value: {
error: {
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
}
}
}
},
'401': {
'CREDENTIAL_REQUIRED': {
value: {
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
}
}
},
'403': {
'AUTHENTICATION_FAILED': {
value: {
error: {
message: 'Authentication failed. Please ensure your token is correct.',
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
}
}
}
},
'418': {
'I_AM_AI': {
value: {
error: {
message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
code: 'I_AM_AI',
id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
}
}
}
},
'429': {
'RATE_LIMIT_EXCEEDED': {
value: {
error: {
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
}
}
}
},
'500': {
'INTERNAL_ERROR': {
value: {
error: {
message: 'Internal error occurred. Please contact us if the error persists.',
code: 'INTERNAL_ERROR',
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
}
}
}
}
};
const schemas = {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
description: 'An error object.',
properties: {
code: {
type: 'string',
description: 'An error code.',
},
message: {
type: 'string',
description: 'An error message.',
},
id: {
type: 'string',
format: 'uuid',
description: 'An error ID. This ID is static.',
}
},
required: ['code', 'id', 'message']
},
},
required: ['error']
},
User: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this User.'
},
username: {
type: 'string',
description: 'The screen name, handle, or alias that this user identifies themselves with.',
example: 'ai'
},
name: {
type: 'string',
nullable: true,
description: 'The name of the user, as theyve defined it.',
example: '藍'
},
host: {
type: 'string',
nullable: true,
example: 'misskey.example.com'
},
description: {
type: 'string',
nullable: true,
description: 'The user-defined UTF-8 string describing their account.',
example: 'Hi masters, I am Ai!'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the user account was created on Misskey.'
},
followersCount: {
type: 'number',
description: 'The number of followers this account currently has.'
},
followingCount: {
type: 'number',
description: 'The number of users this account is following.'
},
notesCount: {
type: 'number',
description: 'The number of Notes (including renotes) issued by the user.'
},
isBot: {
type: 'boolean',
description: 'Whether this account is a bot.'
},
isCat: {
type: 'boolean',
description: 'Whether this account is a cat.'
},
isAdmin: {
type: 'boolean',
description: 'Whether this account is the admin.'
},
isVerified: {
type: 'boolean'
},
isLocked: {
type: 'boolean'
},
},
required: ['id', 'name', 'username', 'createdAt']
},
Note: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this Note.'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the Note was created on Misskey.'
},
text: {
type: 'string'
},
cw: {
type: 'string'
},
userId: {
type: 'string',
format: 'id',
},
user: {
$ref: '#/components/schemas/User'
},
replyId: {
type: 'string',
format: 'id',
},
renoteId: {
type: 'string',
format: 'id',
},
reply: {
$ref: '#/components/schemas/Note'
},
renote: {
$ref: '#/components/schemas/Note'
},
viaMobile: {
type: 'boolean'
},
visibility: {
type: 'string'
},
},
required: ['id', 'userId', 'createdAt']
},
DriveFile: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this Drive file.'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the Drive file was created on Misskey.'
},
name: {
type: 'string',
description: 'The file name with extension.',
example: 'lenna.jpg'
},
type: {
type: 'string',
description: 'The MIME type of this Drive file.',
example: 'image/jpeg'
},
md5: {
type: 'string',
format: 'md5',
description: 'The MD5 hash of this Drive file.',
example: '15eca7fba0480996e2245f5185bf39f2'
},
datasize: {
type: 'number',
description: 'The size of this Drive file. (bytes)',
example: 51469
},
folderId: {
type: 'string',
format: 'id',
nullable: true,
description: 'The parent folder ID of this Drive file.',
},
isSensitive: {
type: 'boolean',
description: 'Whether this Drive file is sensitive.',
},
},
required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
}
};
export function genOpenapiSpec(lang = 'ja-JP') {
const spec = {
openapi: '3.0.0',
info: {
version: 'v1',
title: 'Misskey API',
description: 'Misskey is a decentralized microblogging platform.',
'x-logo': { url: '/assets/api-doc.png' }
},
servers: [{
url: config.api_url
}],
paths: {} as any,
components: {
schemas: schemas,
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'body',
name: 'i'
}
}
}
};
function genProps(props: { [key: string]: Context & { desc: any, default: any }; }) {
const properties = {} as any;
const kvs = Object.entries(props);
for (const kv of kvs) {
properties[kv[0]] = genProp(kv[1], kv[1].desc, kv[1].default);
}
return properties;
}
function genProp(param: Context, desc?: string, _default?: any): any {
const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
return {
description: desc,
default: _default,
...(_default ? { default: _default } : {}),
type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
...(param.name === 'ID' ? { example: 'xxxxxxxxxxxxxxxxxxxxxxxx', format: 'id' } : {}),
nullable: param.isNullable,
...(param.name === 'String' ? {
...((param as any).enum ? { enum: (param as any).enum } : {}),
...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
} : {}),
...(param.name === 'Number' ? {
...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
} : {}),
...(param.name === 'Object' ? {
...(required.length > 0 ? { required } : {}),
properties: (param as any).props ? genProps((param as any).props) : {}
} : {}),
...(param.name === 'Array' ? {
items: (param as any).ctx ? genProp((param as any).ctx) : {}
} : {})
};
}
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
const porops = {} as any;
const errors = {} as any;
if (endpoint.meta.errors) {
for (const e of Object.values(endpoint.meta.errors)) {
errors[e.code] = {
value: {
error: e
}
};
}
}
if (endpoint.meta.params) {
for (const kv of Object.entries(endpoint.meta.params)) {
if (kv[1].desc) (kv[1].validator as any).desc = kv[1].desc[lang];
if (kv[1].default) (kv[1].validator as any).default = kv[1].default;
porops[kv[0]] = kv[1].validator;
}
}
const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
const resSchema = endpoint.meta.res ? renderType(endpoint.meta.res) : {};
function renderType(x: any) {
const res = {} as any;
if (['User', 'Note', 'DriveFile'].includes(x.type)) {
res['$ref'] = `#/components/schemas/${x.type}`;
} else if (x.type === 'object') {
res['type'] = 'object';
if (x.props) {
const props = {} as any;
for (const kv of Object.entries(x.props)) {
props[kv[0]] = renderType(kv[1]);
}
res['properties'] = props;
}
} else if (x.type === 'array') {
res['type'] = 'array';
if (x.items) {
res['items'] = renderType(x.items);
}
} else {
res['type'] = x.type;
}
return res;
}
const info = {
summary: endpoint.name,
description: endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.',
...(endpoint.meta.tags ? {
tags: endpoint.meta.tags
} : {}),
...(endpoint.meta.requireCredential ? {
security: [{
ApiKeyAuth: []
}]
} : {}),
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
...(required.length > 0 ? { required } : {}),
properties: endpoint.meta.params ? genProps(porops) : {}
}
}
}
},
responses: {
...(endpoint.meta.res ? {
'200': {
description: 'OK (with results)',
content: {
'application/json': {
schema: resSchema
}
}
}
} : {
'204': {
description: 'OK (without any results)',
}
}),
'400': {
description: 'Client error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: { ...errors, ...basicErrors['400'] }
}
}
},
'401': {
description: 'Authentication error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['401']
}
}
},
'403': {
description: 'Forbiddon error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['403']
}
}
},
'418': {
description: 'I\'m Ai',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['418']
}
}
},
...(endpoint.meta.limit ? {
'429': {
description: 'To many requests',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['429']
}
}
}
} : {}),
'500': {
description: 'Internal server error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['500']
}
}
},
}
};
spec.paths['/' + endpoint.name] = {
post: info
};
}
return spec;
}

View File

@ -0,0 +1,48 @@
import config from '../../../config';
export const description = `
## Usage
**APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。**
一部のAPIは認証情報(アクセストークン)が必要です。リクエストの際に\`i\`というパラメータでアクセストークンを添付してください。
### 自分のアカウントのアクセストークンを取得する
「設定 > API」で、自分のアクセストークンを取得できます。
> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
### アプリケーションとしてアクセストークンを取得する
直接ユーザーのアクセストークンをアプリケーションが扱うのはセキュリティ上のリスクがあるので、
アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のアクセストークンをMisskeyに発行してもらいます。
#### 1.アプリケーションを登録する
まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。
登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p>
#### 2.ユーザーに認証させる
アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
認証セッションを開始するには、[${config.apiUrl}/auth/session/generate](#operation/auth/session/generate) へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。
リクエスト形式はJSONで、メソッドはPOSTです。
レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
あなたのアプリがコールバックURLを設定している場合、
ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに\`token\`という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
#### 3.ユーザートークンを取得する
ユーザーが連携を許可したら、[${config.apiUrl}/auth/session/userkey](#operation/auth/session/userkey) へリクエストを送信します。
上手くいけば、認証したユーザーのユーザートークンがレスポンスとして取得できます。おめでとうございます!
ユーザートークンが取得できたら、*「ユーザーのユーザートークン+あなたのアプリのシークレットキーをsha256したもの」*をアクセストークンとして、APIにリクエストできます。
アクセストークンの生成方法を擬似コードで表すと次のようになります:
\`\`\` js
const i = sha256(userToken + secretKey);
\`\`\`
`;

View File

@ -0,0 +1,69 @@
export const errors = {
'400': {
'INVALID_PARAM': {
value: {
error: {
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
}
}
}
},
'401': {
'CREDENTIAL_REQUIRED': {
value: {
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
}
}
},
'403': {
'AUTHENTICATION_FAILED': {
value: {
error: {
message: 'Authentication failed. Please ensure your token is correct.',
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
}
}
}
},
'418': {
'I_AM_AI': {
value: {
error: {
message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
code: 'I_AM_AI',
id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
}
}
}
},
'429': {
'RATE_LIMIT_EXCEEDED': {
value: {
error: {
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
}
}
}
},
'500': {
'INTERNAL_ERROR': {
value: {
error: {
message: 'Internal error occurred. Please contact us if the error persists.',
code: 'INTERNAL_ERROR',
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
}
}
}
}
};

View File

@ -0,0 +1,230 @@
import endpoints from '../endpoints';
import { Context } from 'cafy';
import config from '../../../config';
import { errors as basicErrors } from './errors';
import { schemas } from './schemas';
import { description } from './description';
import { convertOpenApiSchema } from '../../../prelude/schema';
export function genOpenapiSpec(lang = 'ja-JP') {
const spec = {
openapi: '3.0.0',
info: {
version: 'v1',
title: 'Misskey API',
description: '**Misskey is a decentralized microblogging platform.**\n\n' + description,
'x-logo': { url: '/assets/api-doc.png' }
},
externalDocs: {
description: 'Repository',
url: 'https://github.com/syuilo/misskey'
},
servers: [{
url: config.apiUrl
}],
paths: {} as any,
components: {
schemas: schemas,
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'body',
name: 'i'
}
}
}
};
function genProps(props: { [key: string]: Context & { desc: any, default: any }; }) {
const properties = {} as any;
const kvs = Object.entries(props);
for (const kv of kvs) {
properties[kv[0]] = genProp(kv[1], kv[1].desc, kv[1].default);
}
return properties;
}
function genProp(param: Context, desc?: string, _default?: any): any {
const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
return {
description: desc,
default: _default,
...(_default ? { default: _default } : {}),
type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
...(param.name === 'ID' ? { example: 'xxxxxxxxxxxxxxxxxxxxxxxx', format: 'id' } : {}),
nullable: param.isNullable,
...(param.name === 'String' ? {
...((param as any).enum ? { enum: (param as any).enum } : {}),
...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
} : {}),
...(param.name === 'Number' ? {
...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
} : {}),
...(param.name === 'Object' ? {
...(required.length > 0 ? { required } : {}),
properties: (param as any).props ? genProps((param as any).props) : {}
} : {}),
...(param.name === 'Array' ? {
items: (param as any).ctx ? genProp((param as any).ctx) : {}
} : {})
};
}
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
const porops = {} as any;
const errors = {} as any;
if (endpoint.meta.errors) {
for (const e of Object.values(endpoint.meta.errors)) {
errors[e.code] = {
value: {
error: e
}
};
}
}
if (endpoint.meta.params) {
for (const kv of Object.entries(endpoint.meta.params)) {
if (kv[1].desc) (kv[1].validator as any).desc = kv[1].desc[lang];
if (kv[1].default) (kv[1].validator as any).default = kv[1].default;
porops[kv[0]] = kv[1].validator;
}
}
const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
const resSchema = endpoint.meta.res ? convertOpenApiSchema(endpoint.meta.res) : {};
const info = {
operationId: endpoint.name,
summary: endpoint.name,
description: endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.',
externalDocs: {
description: 'Source code',
url: `https://github.com/syuilo/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`
},
...(endpoint.meta.tags ? {
tags: endpoint.meta.tags
} : {}),
...(endpoint.meta.requireCredential ? {
security: [{
ApiKeyAuth: []
}]
} : {}),
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
...(required.length > 0 ? { required } : {}),
properties: endpoint.meta.params ? genProps(porops) : {}
}
}
}
},
responses: {
...(endpoint.meta.res ? {
'200': {
description: 'OK (with results)',
content: {
'application/json': {
schema: resSchema
}
}
}
} : {
'204': {
description: 'OK (without any results)',
}
}),
'400': {
description: 'Client error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: { ...errors, ...basicErrors['400'] }
}
}
},
'401': {
description: 'Authentication error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['401']
}
}
},
'403': {
description: 'Forbiddon error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['403']
}
}
},
'418': {
description: 'I\'m Ai',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['418']
}
}
},
...(endpoint.meta.limit ? {
'429': {
description: 'To many requests',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['429']
}
}
}
} : {}),
'500': {
description: 'Internal server error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
},
examples: basicErrors['500']
}
}
},
}
};
spec.paths['/' + endpoint.name] = {
post: info
};
}
return spec;
}

View File

@ -0,0 +1,196 @@
export const schemas = {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
description: 'An error object.',
properties: {
code: {
type: 'string',
description: 'An error code. Unique within the endpoint.',
},
message: {
type: 'string',
description: 'An error message.',
},
id: {
type: 'string',
format: 'uuid',
description: 'An error ID. This ID is static.',
}
},
required: ['code', 'id', 'message']
},
},
required: ['error']
},
User: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this User.'
},
username: {
type: 'string',
description: 'The screen name, handle, or alias that this user identifies themselves with.',
example: 'ai'
},
name: {
type: 'string',
nullable: true,
description: 'The name of the user, as theyve defined it.',
example: '藍'
},
host: {
type: 'string',
nullable: true,
example: 'misskey.example.com'
},
description: {
type: 'string',
nullable: true,
description: 'The user-defined UTF-8 string describing their account.',
example: 'Hi masters, I am Ai!'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the user account was created on Misskey.'
},
followersCount: {
type: 'number',
description: 'The number of followers this account currently has.'
},
followingCount: {
type: 'number',
description: 'The number of users this account is following.'
},
notesCount: {
type: 'number',
description: 'The number of Notes (including renotes) issued by the user.'
},
isBot: {
type: 'boolean',
description: 'Whether this account is a bot.'
},
isCat: {
type: 'boolean',
description: 'Whether this account is a cat.'
},
isAdmin: {
type: 'boolean',
description: 'Whether this account is the admin.'
},
isVerified: {
type: 'boolean'
},
isLocked: {
type: 'boolean'
},
},
required: ['id', 'name', 'username', 'createdAt']
},
Note: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this Note.'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the Note was created on Misskey.'
},
text: {
type: 'string'
},
cw: {
type: 'string'
},
userId: {
type: 'string',
format: 'id',
},
user: {
$ref: '#/components/schemas/User'
},
replyId: {
type: 'string',
format: 'id',
},
renoteId: {
type: 'string',
format: 'id',
},
reply: {
$ref: '#/components/schemas/Note'
},
renote: {
$ref: '#/components/schemas/Note'
},
viaMobile: {
type: 'boolean'
},
visibility: {
type: 'string'
},
},
required: ['id', 'userId', 'createdAt']
},
DriveFile: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
description: 'The unique identifier for this Drive file.'
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'The date that the Drive file was created on Misskey.'
},
name: {
type: 'string',
description: 'The file name with extension.',
example: 'lenna.jpg'
},
type: {
type: 'string',
description: 'The MIME type of this Drive file.',
example: 'image/jpeg'
},
md5: {
type: 'string',
format: 'md5',
description: 'The MD5 hash of this Drive file.',
example: '15eca7fba0480996e2245f5185bf39f2'
},
datasize: {
type: 'number',
description: 'The size of this Drive file. (bytes)',
example: 51469
},
folderId: {
type: 'string',
format: 'id',
nullable: true,
description: 'The parent folder ID of this Drive file.',
},
isSensitive: {
type: 'boolean',
description: 'Whether this Drive file is sensitive.',
},
},
required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
}
};

View File

@ -179,7 +179,7 @@ router.get('/dc/cb', async ctx => {
url: 'https://discordapp.com/api/users/@me',
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
}, (err, response, body) => {
if (err)
@ -263,7 +263,7 @@ router.get('/dc/cb', async ctx => {
url: 'https://discordapp.com/api/users/@me',
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
}, (err, response, body) => {
if (err)

View File

@ -171,7 +171,7 @@ router.get('/gh/cb', async ctx => {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `bearer ${accessToken}`,
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
}, (err, response, body) => {
if (err)
@ -234,7 +234,7 @@ router.get('/gh/cb', async ctx => {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `bearer ${accessToken}`,
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
}, (err, response, body) => {
if (err)

View File

@ -72,7 +72,7 @@ async function fetch(url: string, path: string) {
proxy: config.proxy,
timeout: 10 * 1000,
headers: {
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
});

View File

@ -79,13 +79,13 @@ router.get('/*/*', async ctx => {
showdown.extension('wsUrlExtension', () => ({
type: 'output',
regex: /%WS_URL%/g,
replace: config.ws_url
replace: config.wsUrl
}));
showdown.extension('apiUrlExtension', () => ({
type: 'output',
regex: /%API_URL%/g,
replace: config.api_url
replace: config.apiUrl
}));
const conv = new showdown.Converter({

View File

@ -21,7 +21,7 @@ import getNoteSummary from '../../misc/get-note-summary';
import fetchMeta from '../../misc/fetch-meta';
import Emoji from '../../models/emoji';
import * as pkg from '../../../package.json';
import { genOpenapiSpec } from '../api/gen-openapi-spec';
import { genOpenapiSpec } from '../api/openapi/gen-spec';
const client = `${__dirname}/../../client/`;
@ -55,7 +55,6 @@ router.get('/assets/*', async ctx => {
await send(ctx as any, ctx.path, {
root: client,
maxage: ms('7 days'),
immutable: true
});
});

View File

@ -2,46 +2,74 @@ import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import DriveFile, { IDriveFile } from '../../models/drive-file';
import { isLocalUser } from '../../models/user';
import { SchemaType } from '../../prelude/schema';
/**
* ドライブに関するチャート
*/
type DriveLog = {
local: {
/**
* 集計期間時点での、全ドライブファイル数
*/
totalCount: number;
const logSchema = {
/**
* 集計期間時点での、全ドライブファイル数
*/
totalCount: {
type: 'number' as 'number',
description: '集計期間時点での、全ドライブファイル数'
},
/**
* 集計期間時点での、全ドライブファイルの合計サイズ
*/
totalSize: number;
/**
* 集計期間時点での、全ドライブファイルの合計サイズ
*/
totalSize: {
type: 'number' as 'number',
description: '集計期間時点での、全ドライブファイルの合計サイズ'
},
/**
* 増加したドライブファイル数
*/
incCount: number;
/**
* 増加したドライブファイル数
*/
incCount: {
type: 'number' as 'number',
description: '増加したドライブファイル数'
},
/**
* 増加したドライブ使用量
*/
incSize: number;
/**
* 増加したドライブ使用量
*/
incSize: {
type: 'number' as 'number',
description: '増加したドライブ使用量'
},
/**
* 減少したドライブファイル数
*/
decCount: number;
/**
* 減少したドライブファイル数
*/
decCount: {
type: 'number' as 'number',
description: '減少したドライブファイル数'
},
/**
* 減少したドライブ使用量
*/
decSize: number;
};
remote: DriveLog['local'];
/**
* 減少したドライブ使用量
*/
decSize: {
type: 'number' as 'number',
description: '減少したドライブ使用量'
},
};
export const driveLogSchema = {
type: 'object' as 'object',
properties: {
local: {
type: 'object' as 'object',
properties: logSchema
},
remote: {
type: 'object' as 'object',
properties: logSchema
},
}
};
type DriveLog = SchemaType<typeof driveLogSchema>;
class DriveChart extends Chart<DriveLog> {
constructor() {
super('drive');

View File

@ -9,6 +9,7 @@ import * as mongo from 'mongodb';
import db from '../../db/mongodb';
import { ICollection } from 'monk';
import Logger from '../../misc/logger';
import { Schema } from '../../prelude/schema';
const logger = new Logger('chart');
@ -346,3 +347,18 @@ export default abstract class Chart<T extends Obj> {
return res;
}
}
export function convertLog(logSchema: Schema): Schema {
const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
if (v.type === 'number') {
v.type = 'array';
v.items = {
type: 'number'
};
} else if (v.type === 'object') {
for (const k of Object.keys(v.properties)) {
v.properties[k] = convertLog(v.properties[k]);
}
}
return v;
}

View File

@ -2,48 +2,61 @@ import autobind from 'autobind-decorator';
import Chart, { Obj } from '.';
import Note, { INote } from '../../models/note';
import { isLocalUser } from '../../models/user';
import { SchemaType } from '../../prelude/schema';
/**
* 投稿に関するチャート
*/
type NotesLog = {
local: {
/**
* 集計期間時点での、全投稿数
*/
total: number;
const logSchema = {
total: {
type: 'number' as 'number',
description: '集計期間時点での、全投稿数'
},
/**
* 増加した投稿数
*/
inc: number;
inc: {
type: 'number' as 'number',
description: '増加した投稿数'
},
/**
* 減少した投稿数
*/
dec: number;
dec: {
type: 'number' as 'number',
description: '減少した投稿数'
},
diffs: {
/**
* 通常の投稿数の差分
*/
normal: number;
diffs: {
type: 'object' as 'object',
properties: {
normal: {
type: 'number' as 'number',
description: '通常の投稿数の差分'
},
/**
* リプライの投稿数の差分
*/
reply: number;
reply: {
type: 'number' as 'number',
description: 'リプライの投稿数の差分'
},
/**
* Renoteの投稿数の差分
*/
renote: number;
};
};
remote: NotesLog['local'];
renote: {
type: 'number' as 'number',
description: 'Renoteの投稿数の差分'
},
}
},
};
export const notesLogSchema = {
type: 'object' as 'object',
properties: {
local: {
type: 'object' as 'object',
properties: logSchema
},
remote: {
type: 'object' as 'object',
properties: logSchema
},
}
};
type NotesLog = SchemaType<typeof notesLogSchema>;
class NotesChart extends Chart<NotesLog> {
constructor() {
super('notes');

View File

@ -1,42 +1,63 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import DriveFile, { IDriveFile } from '../../models/drive-file';
import { SchemaType } from '../../prelude/schema';
/**
* ユーザーごとのドライブに関するチャート
*/
type PerUserDriveLog = {
/**
* 集計期間時点での、全ドライブファイル数
*/
totalCount: number;
export const perUserDriveLogSchema = {
type: 'object' as 'object',
properties: {
/**
* 集計期間時点での、全ドライブファイル数
*/
totalCount: {
type: 'number' as 'number',
description: '集計期間時点での、全ドライブファイル数'
},
/**
* 集計期間時点での、全ドライブファイルの合計サイズ
*/
totalSize: number;
/**
* 集計期間時点での、全ドライブファイルの合計サイズ
*/
totalSize: {
type: 'number' as 'number',
description: '集計期間時点での、全ドライブファイルの合計サイズ'
},
/**
* 増加したドライブファイル数
*/
incCount: number;
/**
* 増加したドライブファイル数
*/
incCount: {
type: 'number' as 'number',
description: '増加したドライブファイル数'
},
/**
* 増加したドライブ使用量
*/
incSize: number;
/**
* 増加したドライブ使用量
*/
incSize: {
type: 'number' as 'number',
description: '増加したドライブ使用量'
},
/**
* 減少したドライブファイル数
*/
decCount: number;
/**
* 減少したドライブファイル数
*/
decCount: {
type: 'number' as 'number',
description: '減少したドライブファイル数'
},
/**
* 減少したドライブ使用量
*/
decSize: number;
/**
* 減少したドライブ使用量
*/
decSize: {
type: 'number' as 'number',
description: '減少したドライブ使用量'
},
}
};
type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>;
class PerUserDriveChart extends Chart<PerUserDriveLog> {
constructor() {
super('perUserDrive', true);

View File

@ -2,44 +2,50 @@ import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import Note, { INote } from '../../models/note';
import { IUser } from '../../models/user';
import { SchemaType } from '../../prelude/schema';
/**
* ユーザーごとの投稿に関するチャート
*/
type PerUserNotesLog = {
/**
* 集計期間時点での、全投稿数
*/
total: number;
export const perUserNotesLogSchema = {
type: 'object' as 'object',
properties: {
total: {
type: 'number' as 'number',
description: '集計期間時点での、全投稿数'
},
/**
* 増加した投稿数
*/
inc: number;
inc: {
type: 'number' as 'number',
description: '増加した投稿数'
},
/**
* 減少した投稿数
*/
dec: number;
dec: {
type: 'number' as 'number',
description: '減少した投稿数'
},
diffs: {
/**
* 通常の投稿数の差分
*/
normal: number;
diffs: {
type: 'object' as 'object',
properties: {
normal: {
type: 'number' as 'number',
description: '通常の投稿数の差分'
},
/**
* リプライの投稿数の差分
*/
reply: number;
reply: {
type: 'number' as 'number',
description: 'リプライの投稿数の差分'
},
/**
* Renoteの投稿数の差分
*/
renote: number;
};
renote: {
type: 'number' as 'number',
description: 'Renoteの投稿数の差分'
},
}
},
}
};
type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>;
class PerUserNotesChart extends Chart<PerUserNotesLog> {
constructor() {
super('perUserNotes', true);

View File

@ -1,31 +1,50 @@
import autobind from 'autobind-decorator';
import Chart, { Obj } from './';
import User, { IUser, isLocalUser } from '../../models/user';
import { SchemaType } from '../../prelude/schema';
/**
* ユーザーに関するチャート
*/
type UsersLog = {
local: {
/**
* 集計期間時点での、全ユーザー数
*/
total: number;
const logSchema = {
/**
* 集計期間時点での、全ユーザー数
*/
total: {
type: 'number' as 'number',
description: '集計期間時点での、全ユーザー数'
},
/**
* 増加したユーザー数
*/
inc: number;
/**
* 増加したユーザー数
*/
inc: {
type: 'number' as 'number',
description: '増加したユーザー数'
},
/**
* 減少したユーザー数
*/
dec: number;
};
remote: UsersLog['local'];
/**
* 減少したユーザー数
*/
dec: {
type: 'number' as 'number',
description: '減少したユーザー数'
},
};
export const usersLogSchema = {
type: 'object' as 'object',
properties: {
local: {
type: 'object' as 'object',
properties: logSchema
},
remote: {
type: 'object' as 'object',
properties: logSchema
},
}
};
type UsersLog = SchemaType<typeof usersLogSchema>;
class UsersChart extends Chart<UsersLog> {
constructor() {
super('users');

View File

@ -58,7 +58,7 @@ export default async (
proxy: config.proxy,
timeout: 10 * 1000,
headers: {
'User-Agent': config.user_agent
'User-Agent': config.userAgent
}
});