Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
d4c7ca76ac | |||
1a6aae944f | |||
71e0241c94 | |||
d838ef5b76 | |||
d90a5c9154 | |||
9b79a411e0 | |||
b8e0ec9edc | |||
57009057ae | |||
5db7b2e193 | |||
d994c84901 | |||
febfb97bb4 | |||
a6c5e62923 | |||
ac0390fec3 | |||
97b99867f2 | |||
a55d5516a6 | |||
b679163d01 | |||
76edcdbe45 | |||
d8d51519c4 | |||
3446969121 | |||
0e0c35a701 | |||
c9a6c9e20a | |||
3d2b982a94 | |||
6157d8331b | |||
3d0fc09fae | |||
b975751710 | |||
4530d40537 | |||
94e3ac9b72 | |||
e13fe97ebb | |||
4ad7632113 | |||
0709cac97f | |||
7dd4180fba | |||
558213490a | |||
78a69e3da8 | |||
140c78e5a7 | |||
a8e18e0e22 | |||
2a8bb23625 | |||
936a4d1bc4 | |||
69c34e8d00 |
@ -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 |
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 d’un dossier n’est 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:
|
||||
|
@ -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:
|
||||
|
@ -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: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
16
src/@types/webfinger.js.d.ts
vendored
16
src/@types/webfinger.js.d.ts
vendored
@ -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[];
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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 : ''}`;
|
||||
}
|
||||
|
@ -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) {
|
||||
// 既に連携しているか
|
||||
|
@ -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
38
src/prelude/schema.ts
Normal 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;
|
||||
}
|
@ -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}`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
* このエンドポイントにリクエストするのにユーザー情報が必須か否か
|
||||
|
@ -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}`
|
||||
};
|
||||
});
|
||||
|
@ -10,7 +10,11 @@ export const meta = {
|
||||
|
||||
params: {
|
||||
token: {
|
||||
validator: $.str
|
||||
validator: $.str,
|
||||
desc: {
|
||||
'ja-JP': 'セッションのトークン',
|
||||
'en-US': 'The token of a session.'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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.'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
|
@ -15,8 +15,7 @@ export const meta = {
|
||||
params: {},
|
||||
|
||||
res: {
|
||||
type: 'entity',
|
||||
entity: 'User'
|
||||
type: 'User',
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,12 +175,10 @@ export const meta = {
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
props: {
|
||||
properties: {
|
||||
createdNote: {
|
||||
type: 'Note',
|
||||
desc: {
|
||||
'ja-JP': '作成した投稿'
|
||||
}
|
||||
description: '作成した投稿'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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 they’ve 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;
|
||||
}
|
48
src/server/api/openapi/description.ts
Normal file
48
src/server/api/openapi/description.ts
Normal 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);
|
||||
\`\`\`
|
||||
`;
|
69
src/server/api/openapi/errors.ts
Normal file
69
src/server/api/openapi/errors.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
230
src/server/api/openapi/gen-spec.ts
Normal file
230
src/server/api/openapi/gen-spec.ts
Normal 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;
|
||||
}
|
196
src/server/api/openapi/schemas.ts
Normal file
196
src/server/api/openapi/schemas.ts
Normal 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 they’ve 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']
|
||||
}
|
||||
};
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -58,7 +58,7 @@ export default async (
|
||||
proxy: config.proxy,
|
||||
timeout: 10 * 1000,
|
||||
headers: {
|
||||
'User-Agent': config.user_agent
|
||||
'User-Agent': config.userAgent
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user