Merge branch 'master' of github.com:syuilo/misskey
This commit is contained in:
commit
6c75bc6d51
|
@ -2,7 +2,6 @@
|
||||||
/.vscode
|
/.vscode
|
||||||
/node_modules
|
/node_modules
|
||||||
/built
|
/built
|
||||||
/uploads
|
|
||||||
/data
|
/data
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
*.pem
|
*.pem
|
||||||
|
|
|
@ -22,5 +22,5 @@ elasticsearch:
|
||||||
port: 9200
|
port: 9200
|
||||||
pass: ''
|
pass: ''
|
||||||
recaptcha:
|
recaptcha:
|
||||||
siteKey: hima
|
site_key: hima
|
||||||
secretKey: saku
|
secret_key: saku
|
||||||
|
|
|
@ -22,5 +22,5 @@ elasticsearch:
|
||||||
port: 9200
|
port: 9200
|
||||||
pass: ''
|
pass: ''
|
||||||
recaptcha:
|
recaptcha:
|
||||||
siteKey: hima
|
site_key: hima
|
||||||
secretKey: saku
|
secret_key: saku
|
||||||
|
|
213
CHANGELOG.md
213
CHANGELOG.md
|
@ -2,6 +2,219 @@ ChangeLog (Release Notes)
|
||||||
=========================
|
=========================
|
||||||
主に notable な changes を書いていきます
|
主に notable な changes を書いていきます
|
||||||
|
|
||||||
|
3201 (2017/11/23)
|
||||||
|
-----------------
|
||||||
|
* Twitterログインを実装 (#939)
|
||||||
|
|
||||||
|
3196 (2017/11/23)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3194 (2017/11/23)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3191 (2017/11/23)
|
||||||
|
-----------------
|
||||||
|
* :v:
|
||||||
|
|
||||||
|
3188 (2017/11/22)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3180 (2017/11/21)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3177 (2017/11/21)
|
||||||
|
-----------------
|
||||||
|
* ServiceWorker support
|
||||||
|
* Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ)
|
||||||
|
|
||||||
|
3165 (2017/11/20)
|
||||||
|
-----------------
|
||||||
|
* デスクトップ版でも通知バッジを表示 (#918)
|
||||||
|
* デザインの調整
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3155 (2017/11/20)
|
||||||
|
-----------------
|
||||||
|
* デスクトップ版でユーザーの投稿グラフを見れるように
|
||||||
|
|
||||||
|
3142 (2017/11/18)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3140 (2017/11/18)
|
||||||
|
-----------------
|
||||||
|
* ウィジェットをスクロールに追従させるように
|
||||||
|
|
||||||
|
3136 (2017/11/17)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
* 通信の最適化
|
||||||
|
|
||||||
|
3131 (2017/11/17)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
* 通信の最適化
|
||||||
|
|
||||||
|
3124 (2017/11/16)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3121 (2017/11/16)
|
||||||
|
-----------------
|
||||||
|
* ブロードキャストウィジェットの強化
|
||||||
|
* デザインのグリッチの修正
|
||||||
|
* 通信の最適化
|
||||||
|
|
||||||
|
3113 (2017/11/15)
|
||||||
|
-----------------
|
||||||
|
* アクティビティのレンダリングの問題の修正など
|
||||||
|
|
||||||
|
3110 (2017/11/15)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整など
|
||||||
|
|
||||||
|
3107 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
3104 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* デスクトップ版ユーザーページのデザインの改良
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3099 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* デスクトップ版ユーザーページの強化
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3093 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* やった
|
||||||
|
|
||||||
|
3089 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* なんか
|
||||||
|
|
||||||
|
3069 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* ドライブウィンドウもポップアウトできるように
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
3066 (2017/11/14)
|
||||||
|
-----------------
|
||||||
|
* メッセージウィジェット追加
|
||||||
|
* アクセスログウィジェット追加
|
||||||
|
|
||||||
|
3057 (2017/11/13)
|
||||||
|
-----------------
|
||||||
|
* グリッチ修正
|
||||||
|
|
||||||
|
3055 (2017/11/13)
|
||||||
|
-----------------
|
||||||
|
* メッセージのウィンドウのポップアウト (#911)
|
||||||
|
|
||||||
|
3050 (2017/11/13)
|
||||||
|
-----------------
|
||||||
|
* 通信の最適化
|
||||||
|
* これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる
|
||||||
|
* デザインの調整
|
||||||
|
* ユーザビリティの向上
|
||||||
|
|
||||||
|
3040 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3038 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* 投稿フォームウィジェットの追加
|
||||||
|
* タイムライン上部にもウィジェットを配置できるように
|
||||||
|
|
||||||
|
3035 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* ウィジェットの強化
|
||||||
|
|
||||||
|
3033 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
3031 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* ウィジェットの強化
|
||||||
|
|
||||||
|
3028 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* ウィジェットの表示をコンパクトにできるように
|
||||||
|
|
||||||
|
3026 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
|
||||||
|
3024 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* いい感じにするなど
|
||||||
|
|
||||||
|
3020 (2017/11/12)
|
||||||
|
-----------------
|
||||||
|
* 通信の最適化
|
||||||
|
|
||||||
|
3017 (2017/11/11)
|
||||||
|
-----------------
|
||||||
|
* 誤字修正など
|
||||||
|
|
||||||
|
3012 (2017/11/11)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
3010 (2017/11/11)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
3008 (2017/11/11)
|
||||||
|
-----------------
|
||||||
|
* カレンダー(タイムマシン)ウィジェットの追加
|
||||||
|
|
||||||
|
3006 (2017/11/11)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
* など
|
||||||
|
|
||||||
|
2996 (2017/11/10)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
* など
|
||||||
|
|
||||||
|
2991 (2017/11/09)
|
||||||
|
-----------------
|
||||||
|
* デザインの調整
|
||||||
|
|
||||||
|
2988 (2017/11/09)
|
||||||
|
-----------------
|
||||||
|
* チャンネルウィジェットを追加
|
||||||
|
|
||||||
|
2984 (2017/11/09)
|
||||||
|
-----------------
|
||||||
|
* スライドショーウィジェットを追加
|
||||||
|
|
||||||
|
2974 (2017/11/08)
|
||||||
|
-----------------
|
||||||
|
* ホームのカスタマイズを実装するなど
|
||||||
|
|
||||||
|
2971 (2017/11/08)
|
||||||
|
-----------------
|
||||||
|
* バグ修正
|
||||||
|
* デザインの調整
|
||||||
|
* i18n
|
||||||
|
|
||||||
|
2944 (2017/11/07)
|
||||||
|
-----------------
|
||||||
|
* パフォーマンスの向上
|
||||||
|
* GirdFSになるなどした
|
||||||
|
* 依存関係の更新
|
||||||
|
|
||||||
2807 (2017/11/02)
|
2807 (2017/11/02)
|
||||||
-----------------
|
-----------------
|
||||||
* いい感じに
|
* いい感じに
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
DONORS
|
DONORS
|
||||||
======
|
======
|
||||||
|
The list of people who have sent donation for Misskey.
|
||||||
|
|
||||||
(no particular order)
|
(no particular order)
|
||||||
|
|
||||||
|
@ -7,12 +8,14 @@ DONORS
|
||||||
* 俺様
|
* 俺様
|
||||||
* なぎうり
|
* なぎうり
|
||||||
* スルメ https://surume.tk/
|
* スルメ https://surume.tk/
|
||||||
|
* 藍
|
||||||
|
* 音船 https://otofune.me/
|
||||||
|
|
||||||
:heart: Thanks for donating, guys!
|
:heart: Thanks for donating, guys!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Although you donated, you are not listed here? please contact to us!
|
If your name is missing, please contact us!
|
||||||
|
|
||||||
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
|
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
|
||||||
|
|
||||||
|
|
22
README.md
22
README.md
|
@ -17,7 +17,7 @@ Key features
|
||||||
* Automatically updated timeline
|
* Automatically updated timeline
|
||||||
* Private messages
|
* Private messages
|
||||||
* Free 1GB storage for each all users
|
* Free 1GB storage for each all users
|
||||||
* Machine learning
|
* ServiceWorker support
|
||||||
* Web API for third-party applications
|
* Web API for third-party applications
|
||||||
* No ads
|
* No ads
|
||||||
|
|
||||||
|
@ -38,10 +38,18 @@ Please see [ChangeLog](./CHANGELOG.md).
|
||||||
|
|
||||||
Sponsors & Backers
|
Sponsors & Backers
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
Misskey have no 100+ GitHub stars currently. However, donation are always welcome!
|
Misskey has no 100+ GitHub stars currently. However, a donation is always welcome!
|
||||||
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
|
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
|
||||||
|
|
||||||
**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md).
|
**Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md).
|
||||||
|
|
||||||
|
Collaborators
|
||||||
|
----------------------------------------------------------------
|
||||||
|
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] |
|
||||||
|
|------------------------|-----------------------------------|---------------------------------|
|
||||||
|
| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] |
|
||||||
|
|
||||||
|
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
@ -51,8 +59,8 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
|
||||||
[mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square
|
[mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square
|
||||||
[travis-link]: https://travis-ci.org/syuilo/misskey
|
[travis-link]: https://travis-ci.org/syuilo/misskey
|
||||||
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
|
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
|
||||||
[dependencies-link]: https://gemnasium.com/syuilo/misskey
|
[dependencies-link]: https://david-dm.org/syuilo/misskey
|
||||||
[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square
|
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
|
||||||
[himasaku]: https://himasaku.net
|
[himasaku]: https://himasaku.net
|
||||||
[himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
|
[himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
|
||||||
[sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
|
[sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
|
||||||
|
@ -60,3 +68,7 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
|
||||||
<!-- Collaborators Info -->
|
<!-- Collaborators Info -->
|
||||||
[syuilo-link]: https://syuilo.com
|
[syuilo-link]: https://syuilo.com
|
||||||
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
|
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
|
||||||
|
[ayamorisawa-link]: https://github.com/ayamorisawa
|
||||||
|
[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
|
||||||
|
[otofune-link]: https://github.com/otofune
|
||||||
|
[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
|
||||||
|
|
35
appveyor.yml
35
appveyor.yml
|
@ -1,35 +0,0 @@
|
||||||
# appveyor file
|
|
||||||
# http://www.appveyor.com/docs/appveyor-yml
|
|
||||||
|
|
||||||
branches:
|
|
||||||
except:
|
|
||||||
- release
|
|
||||||
|
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- nodejs_version: 8.4.0
|
|
||||||
|
|
||||||
build: off
|
|
||||||
|
|
||||||
install:
|
|
||||||
# Update Node.js
|
|
||||||
# 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準)
|
|
||||||
- ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
|
|
||||||
- node --version
|
|
||||||
|
|
||||||
# Update NPM
|
|
||||||
- npm install -g npm
|
|
||||||
- npm --version
|
|
||||||
|
|
||||||
# Update node-gyp
|
|
||||||
# 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します
|
|
||||||
- npm install -g node-gyp
|
|
||||||
|
|
||||||
- npm install
|
|
||||||
|
|
||||||
init:
|
|
||||||
# git clone の際の改行を変換しないようにします
|
|
||||||
- git config --global core.autocrlf false
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- npm run build
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
``` yaml
|
||||||
|
# サーバーのメンテナ情報
|
||||||
|
maintainer:
|
||||||
|
# メンテナの名前
|
||||||
|
name:
|
||||||
|
|
||||||
|
# メンテナの連絡先(URLかmailto形式のURL)
|
||||||
|
url:
|
||||||
|
|
||||||
|
# プライマリURL
|
||||||
|
url:
|
||||||
|
|
||||||
|
# セカンダリURL
|
||||||
|
secondary_url:
|
||||||
|
|
||||||
|
# 待受ポート
|
||||||
|
port:
|
||||||
|
|
||||||
|
# TLSの設定(利用しない場合は省略可能)
|
||||||
|
https:
|
||||||
|
# 証明書のパス...
|
||||||
|
key:
|
||||||
|
cert:
|
||||||
|
|
||||||
|
# MongoDBの設定
|
||||||
|
mongodb:
|
||||||
|
host: localhost
|
||||||
|
port: 27017
|
||||||
|
db: misskey
|
||||||
|
user:
|
||||||
|
pass:
|
||||||
|
|
||||||
|
# Redisの設定
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
pass:
|
||||||
|
|
||||||
|
# reCAPTCHAの設定
|
||||||
|
recaptcha:
|
||||||
|
site_key:
|
||||||
|
secret_key:
|
||||||
|
|
||||||
|
# ServiceWrokerの設定
|
||||||
|
sw:
|
||||||
|
# VAPIDの公開鍵
|
||||||
|
public_key:
|
||||||
|
|
||||||
|
# VAPIDの秘密鍵
|
||||||
|
private_key:
|
||||||
|
|
||||||
|
```
|
|
@ -1,7 +1,7 @@
|
||||||
Misskey Setup and Installation Guide
|
Misskey Setup and Installation Guide
|
||||||
================================================================
|
================================================================
|
||||||
|
|
||||||
We thank you for your interest in setup your Misskey server!
|
We thank you for your interest in setting up your Misskey server!
|
||||||
This guide describes how to install and setup Misskey.
|
This guide describes how to install and setup Misskey.
|
||||||
|
|
||||||
[Japanese version also available - 日本語版もあります](./setup.ja.md)
|
[Japanese version also available - 日本語版もあります](./setup.ja.md)
|
||||||
|
@ -36,6 +36,15 @@ Note that Misskey uses following subdomains:
|
||||||
Misskey requires reCAPTCHA tokens.
|
Misskey requires reCAPTCHA tokens.
|
||||||
Please visit https://www.google.com/recaptcha/intro/ and generate keys.
|
Please visit https://www.google.com/recaptcha/intro/ and generate keys.
|
||||||
|
|
||||||
|
*(optional)* Generating VAPID keys
|
||||||
|
----------------------------------------------------------------
|
||||||
|
If you want to enable ServiceWroker, you need to generate VAPID keys:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
npm install web-push -g
|
||||||
|
web-push generate-vapid-keys
|
||||||
|
```
|
||||||
|
|
||||||
*3.* Install dependencies
|
*3.* Install dependencies
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
Please install and setup these softwares:
|
Please install and setup these softwares:
|
||||||
|
@ -51,24 +60,6 @@ Please install and setup these softwares:
|
||||||
|
|
||||||
*4.* Install Misskey
|
*4.* Install Misskey
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
There is **two ways** to install Misskey:
|
|
||||||
|
|
||||||
### WAY 1) Using built code (recommended)
|
|
||||||
We have official release of Misskey.
|
|
||||||
The built code is automatically pushed to https://github.com/syuilo/misskey/tree/release after the CI test succeeds.
|
|
||||||
|
|
||||||
1. `git clone -b release git://github.com/syuilo/misskey.git`
|
|
||||||
2. `cd misskey`
|
|
||||||
3. `npm install`
|
|
||||||
|
|
||||||
#### Update
|
|
||||||
1. `git fetch`
|
|
||||||
2. `git reset --hard origin/release`
|
|
||||||
3. `npm install`
|
|
||||||
|
|
||||||
### WAY 2) Using source code
|
|
||||||
If you want to build Misskey manually, you can do it via the
|
|
||||||
`build` command after download the source code of Misskey and install dependencies:
|
|
||||||
|
|
||||||
1. `git clone -b master git://github.com/syuilo/misskey.git`
|
1. `git clone -b master git://github.com/syuilo/misskey.git`
|
||||||
2. `cd misskey`
|
2. `cd misskey`
|
||||||
|
|
|
@ -37,6 +37,15 @@ Misskeyは以下のサブドメインを使います:
|
||||||
MisskeyはreCAPTCHAトークンを必要とします。
|
MisskeyはreCAPTCHAトークンを必要とします。
|
||||||
https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。
|
https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。
|
||||||
|
|
||||||
|
*(オプション)* VAPIDキーペアの生成
|
||||||
|
----------------------------------------------------------------
|
||||||
|
ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
npm install web-push -g
|
||||||
|
web-push generate-vapid-keys
|
||||||
|
```
|
||||||
|
|
||||||
*3.* 依存関係をインストールする
|
*3.* 依存関係をインストールする
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
これらのソフトウェアをインストール・設定してください:
|
これらのソフトウェアをインストール・設定してください:
|
||||||
|
@ -52,26 +61,6 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生
|
||||||
|
|
||||||
*4.* Misskeyのインストール
|
*4.* Misskeyのインストール
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
Misskeyをインストールするには**2つの方法**があります:
|
|
||||||
|
|
||||||
### 方法 1) ビルドされたコードを利用する (推奨)
|
|
||||||
Misskeyには公式のリリースがあります。
|
|
||||||
ビルドされたコードはCIテストに合格した後、自動で https://github.com/syuilo/misskey/tree/release にpushされています。
|
|
||||||
|
|
||||||
1. `git clone -b release git://github.com/syuilo/misskey.git`
|
|
||||||
2. `cd misskey`
|
|
||||||
3. `npm install`
|
|
||||||
|
|
||||||
#### アップデートするには:
|
|
||||||
1. `git fetch`
|
|
||||||
2. `git reset --hard origin/release`
|
|
||||||
3. `npm install`
|
|
||||||
|
|
||||||
### 方法 2) ソースコードを利用する
|
|
||||||
> 注: この方法では正しくビルド・動作できることは保証されません。
|
|
||||||
|
|
||||||
Misskeyを手動でビルドしたい場合は、Misskeyのソースコードと依存関係をインストールした後、
|
|
||||||
`build`コマンドを用いることができます:
|
|
||||||
|
|
||||||
1. `git clone -b master git://github.com/syuilo/misskey.git`
|
1. `git clone -b master git://github.com/syuilo/misskey.git`
|
||||||
2. `cd misskey`
|
2. `cd misskey`
|
||||||
|
|
14
gulpfile.ts
14
gulpfile.ts
|
@ -13,7 +13,7 @@ import cssnano = require('gulp-cssnano');
|
||||||
import * as uglifyComposer from 'gulp-uglify/composer';
|
import * as uglifyComposer from 'gulp-uglify/composer';
|
||||||
import pug = require('gulp-pug');
|
import pug = require('gulp-pug');
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import imagemin = require('gulp-imagemin');
|
import imagemin = require('gulp-imagemin');
|
||||||
import * as rename from 'gulp-rename';
|
import * as rename from 'gulp-rename';
|
||||||
import * as mocha from 'gulp-mocha';
|
import * as mocha from 'gulp-mocha';
|
||||||
|
@ -81,9 +81,19 @@ gulp.task('lint', () =>
|
||||||
.pipe(tslint.report())
|
.pipe(tslint.report())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task('format', () =>
|
||||||
|
gulp.src('./src/**/*.ts')
|
||||||
|
.pipe(tslint({
|
||||||
|
formatter: 'verbose',
|
||||||
|
fix: true
|
||||||
|
}))
|
||||||
|
.pipe(tslint.report())
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task('mocha', () =>
|
gulp.task('mocha', () =>
|
||||||
gulp.src([])
|
gulp.src([])
|
||||||
.pipe(mocha({
|
.pipe(mocha({
|
||||||
|
exit: true
|
||||||
//compilers: 'ts:ts-node/register'
|
//compilers: 'ts:ts-node/register'
|
||||||
} as any))
|
} as any))
|
||||||
);
|
);
|
||||||
|
@ -123,7 +133,7 @@ gulp.task('build:client:script', () =>
|
||||||
.pipe(replace('VERSION', JSON.stringify(version)))
|
.pipe(replace('VERSION', JSON.stringify(version)))
|
||||||
.pipe(isProduction ? uglify({
|
.pipe(isProduction ? uglify({
|
||||||
toplevel: true
|
toplevel: true
|
||||||
}) : gutil.noop())
|
} as any) : gutil.noop())
|
||||||
.pipe(gulp.dest('./built/web/assets/')) as any
|
.pipe(gulp.dest('./built/web/assets/')) as any
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,15 @@ common:
|
||||||
months_ago: "{}month(s) ago"
|
months_ago: "{}month(s) ago"
|
||||||
years_ago: "{}year(s) ago"
|
years_ago: "{}year(s) ago"
|
||||||
|
|
||||||
|
weekday-short:
|
||||||
|
sunday: "S"
|
||||||
|
monday: "M"
|
||||||
|
tuesday: "T"
|
||||||
|
wednesday: "W"
|
||||||
|
thursday: "T"
|
||||||
|
friday: "F"
|
||||||
|
satruday: "S"
|
||||||
|
|
||||||
reactions:
|
reactions:
|
||||||
like: "Like"
|
like: "Like"
|
||||||
love: "Love"
|
love: "Love"
|
||||||
|
@ -41,6 +50,15 @@ common:
|
||||||
my-token-regenerated: "Your token is just regenerated, so you will signout."
|
my-token-regenerated: "Your token is just regenerated, so you will signout."
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
|
mk-nav-links:
|
||||||
|
about: "About"
|
||||||
|
stats: "Stats"
|
||||||
|
status: "Status"
|
||||||
|
wiki: "Wiki"
|
||||||
|
donors: "Donors"
|
||||||
|
repository: "Repository"
|
||||||
|
develop: "Developers"
|
||||||
|
|
||||||
mk-messaging-form:
|
mk-messaging-form:
|
||||||
attach-from-local: "Attach file from your pc"
|
attach-from-local: "Attach file from your pc"
|
||||||
attach-from-drive: "Attach file from the drive"
|
attach-from-drive: "Attach file from the drive"
|
||||||
|
@ -225,7 +243,6 @@ desktop:
|
||||||
mk-drive-browser-file:
|
mk-drive-browser-file:
|
||||||
avatar: "Avatar"
|
avatar: "Avatar"
|
||||||
banner: "Banner"
|
banner: "Banner"
|
||||||
wallpaper: "Wallpaper"
|
|
||||||
|
|
||||||
mk-drive-browser-folder-contextmenu:
|
mk-drive-browser-folder-contextmenu:
|
||||||
move-to-this-folder: "Move to this folder"
|
move-to-this-folder: "Move to this folder"
|
||||||
|
@ -242,14 +259,11 @@ desktop:
|
||||||
mk-drive-browser-nav-folder:
|
mk-drive-browser-nav-folder:
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
|
|
||||||
mk-nav-home-widget:
|
mk-selectdrive-page:
|
||||||
about: "About"
|
title: "Choose a file(s)"
|
||||||
stats: "Stats"
|
ok: "OK"
|
||||||
status: "Status"
|
cancel: "Cancel"
|
||||||
wiki: "Wiki"
|
upload: "Upload a file(s) from you PC"
|
||||||
donors: "Donors"
|
|
||||||
repository: "Repository"
|
|
||||||
develop: "Developers"
|
|
||||||
|
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "Home"
|
home: "Home"
|
||||||
|
@ -267,6 +281,12 @@ desktop:
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
signout: "Sign out"
|
signout: "Sign out"
|
||||||
|
|
||||||
|
mk-ui-header-post-button:
|
||||||
|
post: "Compose new Post"
|
||||||
|
|
||||||
|
mk-ui-header-notifications:
|
||||||
|
title: "Notifications"
|
||||||
|
|
||||||
mk-password-setting:
|
mk-password-setting:
|
||||||
reset: "Change your password"
|
reset: "Change your password"
|
||||||
enter-current-password: "Enter the current password"
|
enter-current-password: "Enter the current password"
|
||||||
|
@ -327,7 +347,7 @@ desktop:
|
||||||
title: "Server info"
|
title: "Server info"
|
||||||
toggle: "Toggle views"
|
toggle: "Toggle views"
|
||||||
|
|
||||||
mk-activity-home-widget:
|
mk-activity-widget:
|
||||||
title: "Activity"
|
title: "Activity"
|
||||||
toggle: "Toggle views"
|
toggle: "Toggle views"
|
||||||
|
|
||||||
|
@ -354,6 +374,34 @@ desktop:
|
||||||
title: "Donation"
|
title: "Donation"
|
||||||
text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!"
|
text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!"
|
||||||
|
|
||||||
|
mk-channel-home-widget:
|
||||||
|
title: "Channel"
|
||||||
|
settings: "Widget settings"
|
||||||
|
get-started: "Please click the cog in the upper right to specify the channel to receive"
|
||||||
|
|
||||||
|
mk-calendar-widget:
|
||||||
|
title: "{1} / {2}"
|
||||||
|
prev: "Previous month"
|
||||||
|
next: "Next month"
|
||||||
|
go: "Click to travel"
|
||||||
|
|
||||||
|
mk-post-form-home-widget:
|
||||||
|
title: "Post"
|
||||||
|
post: "Post"
|
||||||
|
placeholder: "What's happening?"
|
||||||
|
|
||||||
|
mk-access-log-home-widget:
|
||||||
|
title: "Access log"
|
||||||
|
|
||||||
|
mk-messaging-home-widget:
|
||||||
|
title: "Messaging"
|
||||||
|
|
||||||
|
mk-broadcast-home-widget:
|
||||||
|
fetching: "Fetching"
|
||||||
|
no-broadcasts: "No broadcasts"
|
||||||
|
have-a-nice-day: "Have a nice day!"
|
||||||
|
next: "Next"
|
||||||
|
|
||||||
mk-repost-form:
|
mk-repost-form:
|
||||||
quote: "Quote..."
|
quote: "Quote..."
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
|
@ -365,6 +413,24 @@ desktop:
|
||||||
mk-repost-form-window:
|
mk-repost-form-window:
|
||||||
title: "Are you sure you want to repost this post?"
|
title: "Are you sure you want to repost this post?"
|
||||||
|
|
||||||
|
mk-user:
|
||||||
|
last-used-at: "Last used at"
|
||||||
|
|
||||||
|
photos:
|
||||||
|
title: "Photos"
|
||||||
|
loading: "Loading"
|
||||||
|
no-photos: "No photos"
|
||||||
|
|
||||||
|
frequently-replied-users:
|
||||||
|
title: "Frequently replied"
|
||||||
|
loading: "Loading"
|
||||||
|
no-users: "No users"
|
||||||
|
|
||||||
|
followers-you-know:
|
||||||
|
title: "Followers you know"
|
||||||
|
loading: "Loading"
|
||||||
|
no-users: "No users"
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
mk-selectdrive-page:
|
mk-selectdrive-page:
|
||||||
|
@ -374,7 +440,7 @@ mobile:
|
||||||
download: "Download"
|
download: "Download"
|
||||||
rename: "Rename"
|
rename: "Rename"
|
||||||
move: "Move"
|
move: "Move"
|
||||||
hash: "Hash"
|
hash: "Hash (md5)"
|
||||||
|
|
||||||
mk-entrance-signin:
|
mk-entrance-signin:
|
||||||
signup: "Sign up"
|
signup: "Sign up"
|
||||||
|
|
|
@ -13,6 +13,15 @@ common:
|
||||||
months_ago: "{}ヶ月前"
|
months_ago: "{}ヶ月前"
|
||||||
years_ago: "{}年前"
|
years_ago: "{}年前"
|
||||||
|
|
||||||
|
weekday-short:
|
||||||
|
sunday: "日"
|
||||||
|
monday: "月"
|
||||||
|
tuesday: "火"
|
||||||
|
wednesday: "水"
|
||||||
|
thursday: "木"
|
||||||
|
friday: "金"
|
||||||
|
satruday: "土"
|
||||||
|
|
||||||
reactions:
|
reactions:
|
||||||
like: "いいね"
|
like: "いいね"
|
||||||
love: "ハート"
|
love: "ハート"
|
||||||
|
@ -41,6 +50,15 @@ common:
|
||||||
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
|
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
|
mk-nav-links:
|
||||||
|
about: "Misskeyについて"
|
||||||
|
stats: "統計"
|
||||||
|
status: "ステータス"
|
||||||
|
wiki: "Wiki"
|
||||||
|
donors: "ドナー"
|
||||||
|
repository: "リポジトリ"
|
||||||
|
develop: "開発者"
|
||||||
|
|
||||||
mk-messaging-form:
|
mk-messaging-form:
|
||||||
attach-from-local: "PCからファイルを添付する"
|
attach-from-local: "PCからファイルを添付する"
|
||||||
attach-from-drive: "ドライブからファイルを添付する"
|
attach-from-drive: "ドライブからファイルを添付する"
|
||||||
|
@ -225,7 +243,6 @@ desktop:
|
||||||
mk-drive-browser-file:
|
mk-drive-browser-file:
|
||||||
avatar: "アバター"
|
avatar: "アバター"
|
||||||
banner: "バナー"
|
banner: "バナー"
|
||||||
wallpaper: "壁紙"
|
|
||||||
|
|
||||||
mk-drive-browser-folder-contextmenu:
|
mk-drive-browser-folder-contextmenu:
|
||||||
move-to-this-folder: "このフォルダへ移動"
|
move-to-this-folder: "このフォルダへ移動"
|
||||||
|
@ -242,14 +259,11 @@ desktop:
|
||||||
mk-drive-browser-nav-folder:
|
mk-drive-browser-nav-folder:
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
|
|
||||||
mk-nav-home-widget:
|
mk-selectdrive-page:
|
||||||
about: "Misskeyについて"
|
title: "ファイルを選択してください"
|
||||||
stats: "統計"
|
ok: "決定"
|
||||||
status: "ステータス"
|
cancel: "キャンセル"
|
||||||
wiki: "Wiki"
|
upload: "PCからドライブにファイルをアップロード"
|
||||||
donors: "ドナー"
|
|
||||||
repository: "リポジトリ"
|
|
||||||
develop: "開発者"
|
|
||||||
|
|
||||||
mk-ui-header-nav:
|
mk-ui-header-nav:
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
|
@ -267,6 +281,12 @@ desktop:
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
|
|
||||||
|
mk-ui-header-post-button:
|
||||||
|
post: "新規投稿"
|
||||||
|
|
||||||
|
mk-ui-header-notifications:
|
||||||
|
title: "通知"
|
||||||
|
|
||||||
mk-password-setting:
|
mk-password-setting:
|
||||||
reset: "パスワードを変更する"
|
reset: "パスワードを変更する"
|
||||||
enter-current-password: "現在のパスワードを入力してください"
|
enter-current-password: "現在のパスワードを入力してください"
|
||||||
|
@ -327,7 +347,7 @@ desktop:
|
||||||
title: "サーバー情報"
|
title: "サーバー情報"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
|
||||||
mk-activity-home-widget:
|
mk-activity-widget:
|
||||||
title: "アクティビティ"
|
title: "アクティビティ"
|
||||||
toggle: "表示を切り替え"
|
toggle: "表示を切り替え"
|
||||||
|
|
||||||
|
@ -354,6 +374,34 @@ desktop:
|
||||||
title: "寄付のお願い"
|
title: "寄付のお願い"
|
||||||
text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。"
|
text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。"
|
||||||
|
|
||||||
|
mk-channel-home-widget:
|
||||||
|
title: "チャンネル"
|
||||||
|
settings: "ウィジェットの設定"
|
||||||
|
get-started: "右上の歯車をクリックして受信するチャンネルを指定してください"
|
||||||
|
|
||||||
|
mk-calendar-widget:
|
||||||
|
title: "{1}年 {2}月"
|
||||||
|
prev: "先月"
|
||||||
|
next: "来月"
|
||||||
|
go: "クリックして時間遡行"
|
||||||
|
|
||||||
|
mk-post-form-home-widget:
|
||||||
|
title: "投稿"
|
||||||
|
post: "投稿"
|
||||||
|
placeholder: "いまどうしてる?"
|
||||||
|
|
||||||
|
mk-access-log-home-widget:
|
||||||
|
title: "アクセスログ"
|
||||||
|
|
||||||
|
mk-messaging-home-widget:
|
||||||
|
title: "メッセージ"
|
||||||
|
|
||||||
|
mk-broadcast-home-widget:
|
||||||
|
fetching: "確認中"
|
||||||
|
no-broadcasts: "お知らせはありません"
|
||||||
|
have-a-nice-day: "良い一日を!"
|
||||||
|
next: "次"
|
||||||
|
|
||||||
mk-repost-form:
|
mk-repost-form:
|
||||||
quote: "引用する..."
|
quote: "引用する..."
|
||||||
cancel: "キャンセル"
|
cancel: "キャンセル"
|
||||||
|
@ -365,6 +413,24 @@ desktop:
|
||||||
mk-repost-form-window:
|
mk-repost-form-window:
|
||||||
title: "この投稿をRepostしますか?"
|
title: "この投稿をRepostしますか?"
|
||||||
|
|
||||||
|
mk-user:
|
||||||
|
last-used-at: "最終アクセス"
|
||||||
|
|
||||||
|
photos:
|
||||||
|
title: "フォト"
|
||||||
|
loading: "読み込み中"
|
||||||
|
no-photos: "写真はありません"
|
||||||
|
|
||||||
|
frequently-replied-users:
|
||||||
|
title: "よく話すユーザー"
|
||||||
|
loading: "読み込み中"
|
||||||
|
no-users: "よく話すユーザーはいません"
|
||||||
|
|
||||||
|
followers-you-know:
|
||||||
|
title: "知り合いのフォロワー"
|
||||||
|
loading: "読み込み中"
|
||||||
|
no-users: "知り合いのフォロワーはいません"
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
tags:
|
tags:
|
||||||
mk-selectdrive-page:
|
mk-selectdrive-page:
|
||||||
|
@ -374,7 +440,7 @@ mobile:
|
||||||
download: "ダウンロード"
|
download: "ダウンロード"
|
||||||
rename: "名前を変更"
|
rename: "名前を変更"
|
||||||
move: "移動"
|
move: "移動"
|
||||||
hash: "ハッシュ"
|
hash: "ハッシュ (md5)"
|
||||||
|
|
||||||
mk-entrance-signin:
|
mk-entrance-signin:
|
||||||
signup: "新規登録"
|
signup: "新規登録"
|
||||||
|
|
320
package.json
320
package.json
|
@ -1,158 +1,166 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "0.0.2807",
|
"version": "0.0.3201",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "A miniblog-based SNS",
|
"description": "A miniblog-based SNS",
|
||||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||||
"repository": "https://github.com/syuilo/misskey.git",
|
"repository": "https://github.com/syuilo/misskey.git",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"config": "node ./tools/init.js",
|
"config": "node ./tools/init.js",
|
||||||
"start": "node ./built",
|
"start": "node ./built",
|
||||||
"debug": "DEBUG=misskey:* node ./built",
|
"debug": "DEBUG=misskey:* node ./built",
|
||||||
"swagger": "node ./swagger.js",
|
"swagger": "node ./swagger.js",
|
||||||
"build": "gulp build",
|
"build": "gulp build",
|
||||||
"rebuild": "gulp rebuild",
|
"rebuild": "gulp rebuild",
|
||||||
"clean": "gulp clean",
|
"clean": "gulp clean",
|
||||||
"cleanall": "gulp cleanall",
|
"cleanall": "gulp cleanall",
|
||||||
"lint": "gulp lint",
|
"lint": "gulp lint",
|
||||||
"test": "gulp test"
|
"test": "gulp test",
|
||||||
},
|
"format": "gulp format"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/bcryptjs": "2.4.0",
|
"dependencies": {
|
||||||
"@types/body-parser": "1.16.5",
|
"@prezzemolo/rap": "0.1.2",
|
||||||
"@types/chai": "4.0.4",
|
"@prezzemolo/zip": "0.0.3",
|
||||||
"@types/chai-http": "3.0.3",
|
"@types/bcryptjs": "2.4.1",
|
||||||
"@types/chalk": "0.4.31",
|
"@types/body-parser": "1.16.8",
|
||||||
"@types/compression": "0.0.34",
|
"@types/chai": "4.0.5",
|
||||||
"@types/cors": "2.8.1",
|
"@types/chai-http": "3.0.3",
|
||||||
"@types/debug": "0.0.30",
|
"@types/compression": "0.0.35",
|
||||||
"@types/deep-equal": "1.0.1",
|
"@types/cookie": "0.3.1",
|
||||||
"@types/elasticsearch": "5.0.14",
|
"@types/cors": "2.8.3",
|
||||||
"@types/event-stream": "3.3.32",
|
"@types/debug": "0.0.30",
|
||||||
"@types/express": "4.0.37",
|
"@types/deep-equal": "1.0.1",
|
||||||
"@types/gm": "1.17.32",
|
"@types/elasticsearch": "5.0.17",
|
||||||
"@types/gulp": "4.0.3",
|
"@types/event-stream": "3.3.33",
|
||||||
"@types/gulp-htmlmin": "1.3.30",
|
"@types/eventemitter3": "2.0.2",
|
||||||
"@types/gulp-mocha": "0.0.30",
|
"@types/express": "4.0.39",
|
||||||
"@types/gulp-rename": "0.0.32",
|
"@types/gm": "1.17.33",
|
||||||
"@types/gulp-replace": "0.0.30",
|
"@types/gulp": "4.0.3",
|
||||||
"@types/gulp-tslint": "3.6.31",
|
"@types/gulp-htmlmin": "1.3.31",
|
||||||
"@types/gulp-typescript": "2.13.0",
|
"@types/gulp-mocha": "0.0.31",
|
||||||
"@types/gulp-uglify": "0.0.30",
|
"@types/gulp-rename": "0.0.33",
|
||||||
"@types/gulp-util": "3.0.31",
|
"@types/gulp-replace": "0.0.31",
|
||||||
"@types/inquirer": "0.0.34",
|
"@types/gulp-uglify": "3.0.3",
|
||||||
"@types/is-root": "1.0.0",
|
"@types/gulp-util": "3.0.34",
|
||||||
"@types/is-url": "1.2.28",
|
"@types/inquirer": "0.0.35",
|
||||||
"@types/js-yaml": "3.9.0",
|
"@types/is-root": "1.0.0",
|
||||||
"@types/mocha": "2.2.43",
|
"@types/is-url": "1.2.28",
|
||||||
"@types/mongodb": "2.2.13",
|
"@types/js-yaml": "3.10.0",
|
||||||
"@types/monk": "1.0.6",
|
"@types/mocha": "2.2.44",
|
||||||
"@types/morgan": "1.7.33",
|
"@types/mongodb": "2.2.15",
|
||||||
"@types/ms": "0.7.30",
|
"@types/monk": "1.0.6",
|
||||||
"@types/multer": "1.3.2",
|
"@types/morgan": "1.7.35",
|
||||||
"@types/node": "8.0.33",
|
"@types/ms": "0.7.30",
|
||||||
"@types/ratelimiter": "2.1.28",
|
"@types/multer": "1.3.6",
|
||||||
"@types/redis": "2.6.0",
|
"@types/node": "8.0.53",
|
||||||
"@types/request": "2.0.4",
|
"@types/page": "1.5.32",
|
||||||
"@types/rimraf": "2.0.0",
|
"@types/proxy-addr": "2.0.0",
|
||||||
"@types/riot": "3.6.0",
|
"@types/ratelimiter": "2.1.28",
|
||||||
"@types/serve-favicon": "2.2.28",
|
"@types/redis": "2.8.1",
|
||||||
"@types/uuid": "3.4.2",
|
"@types/request": "2.0.7",
|
||||||
"@types/webpack": "3.0.13",
|
"@types/rimraf": "2.0.2",
|
||||||
"@types/webpack-stream": "3.2.7",
|
"@types/riot": "3.6.1",
|
||||||
"@types/websocket": "0.0.34",
|
"@types/seedrandom": "2.4.27",
|
||||||
"awesome-typescript-loader": "3.2.3",
|
"@types/serve-favicon": "2.2.30",
|
||||||
"chai": "4.1.2",
|
"@types/tmp": "0.0.33",
|
||||||
"chai-http": "3.0.0",
|
"@types/uuid": "3.4.3",
|
||||||
"css-loader": "0.28.7",
|
"@types/webpack": "3.8.1",
|
||||||
"event-stream": "3.3.4",
|
"@types/webpack-stream": "3.2.8",
|
||||||
"gulp": "3.9.1",
|
"@types/websocket": "0.0.34",
|
||||||
"gulp-cssnano": "2.1.2",
|
"accesses": "2.5.0",
|
||||||
"gulp-htmlmin": "3.0.0",
|
"animejs": "2.2.0",
|
||||||
"gulp-imagemin": "3.4.0",
|
"autwh": "0.0.1",
|
||||||
"gulp-mocha": "4.3.1",
|
"awesome-typescript-loader": "3.4.0",
|
||||||
"gulp-pug": "3.3.0",
|
"bcryptjs": "2.4.3",
|
||||||
"gulp-rename": "1.2.2",
|
"body-parser": "1.18.2",
|
||||||
"gulp-replace": "0.6.1",
|
"cafy": "3.2.0",
|
||||||
"gulp-tslint": "8.1.2",
|
"chai": "4.1.2",
|
||||||
"gulp-typescript": "3.2.2",
|
"chai-http": "3.0.0",
|
||||||
"gulp-uglify": "3.0.0",
|
"chalk": "2.3.0",
|
||||||
"gulp-util": "3.0.8",
|
"compression": "1.7.1",
|
||||||
"mocha": "3.5.3",
|
"cookie": "0.3.1",
|
||||||
"riot-tag-loader": "1.0.0",
|
"cors": "2.8.4",
|
||||||
"string-replace-webpack-plugin": "0.1.3",
|
"cropperjs": "1.1.3",
|
||||||
"style-loader": "0.19.0",
|
"css-loader": "0.28.7",
|
||||||
"stylus": "0.54.5",
|
"debug": "3.1.0",
|
||||||
"stylus-loader": "3.0.1",
|
"deep-equal": "1.0.1",
|
||||||
"swagger-jsdoc": "1.9.7",
|
"deepcopy": "0.6.3",
|
||||||
"tslint": "5.7.0",
|
"diskusage": "0.2.4",
|
||||||
"uglify-es": "3.0.27",
|
"elasticsearch": "14.0.0",
|
||||||
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
|
"escape-regexp": "0.0.1",
|
||||||
"uglifyjs-webpack-plugin": "1.0.0-beta.2",
|
"event-stream": "3.3.4",
|
||||||
"webpack": "3.8.1"
|
"eventemitter3": "2.0.3",
|
||||||
},
|
"express": "4.16.2",
|
||||||
"dependencies": {
|
"file-type": "7.3.0",
|
||||||
"accesses": "2.5.0",
|
"fuckadblock": "3.2.1",
|
||||||
"animejs": "2.2.0",
|
"gm": "1.23.0",
|
||||||
"autwh": "0.0.1",
|
"gulp": "3.9.1",
|
||||||
"bcryptjs": "2.4.3",
|
"gulp-cssnano": "2.1.2",
|
||||||
"body-parser": "1.18.2",
|
"gulp-htmlmin": "3.0.0",
|
||||||
"cafy": "3.0.0",
|
"gulp-imagemin": "4.0.0",
|
||||||
"chalk": "2.1.0",
|
"gulp-mocha": "4.3.1",
|
||||||
"compression": "1.7.1",
|
"gulp-pug": "3.3.0",
|
||||||
"cors": "2.8.4",
|
"gulp-rename": "1.2.2",
|
||||||
"cropperjs": "1.1.3",
|
"gulp-replace": "0.6.1",
|
||||||
"crypto": "1.0.1",
|
"gulp-tslint": "8.1.2",
|
||||||
"debug": "3.1.0",
|
"gulp-typescript": "3.2.3",
|
||||||
"deep-equal": "1.0.1",
|
"gulp-uglify": "3.0.0",
|
||||||
"deepcopy": "0.6.3",
|
"gulp-util": "3.0.8",
|
||||||
"diskusage": "0.2.2",
|
"inquirer": "4.0.0",
|
||||||
"download": "6.2.5",
|
"is-root": "1.0.0",
|
||||||
"elasticsearch": "13.3.1",
|
"is-url": "1.2.2",
|
||||||
"escape-regexp": "0.0.1",
|
"js-yaml": "3.10.0",
|
||||||
"express": "4.15.4",
|
"mecab-async": "0.1.0",
|
||||||
"file-type": "6.2.0",
|
"mocha": "4.0.1",
|
||||||
"fuckadblock": "3.2.1",
|
"moji": "0.5.1",
|
||||||
"gm": "1.23.0",
|
"mongodb": "2.2.33",
|
||||||
"inquirer": "3.3.0",
|
"monk": "6.0.5",
|
||||||
"is-root": "1.0.0",
|
"morgan": "1.9.0",
|
||||||
"is-url": "1.2.2",
|
"ms": "2.0.0",
|
||||||
"js-yaml": "3.10.0",
|
"multer": "1.3.0",
|
||||||
"mecab-async": "^0.1.0",
|
"nprogress": "0.2.0",
|
||||||
"moji": "^0.5.1",
|
"os-utils": "0.0.14",
|
||||||
"mongodb": "2.2.33",
|
"page": "1.7.1",
|
||||||
"monk": "6.0.5",
|
"pictograph": "2.1.2",
|
||||||
"morgan": "1.9.0",
|
"prominence": "0.2.0",
|
||||||
"ms": "2.0.0",
|
"proxy-addr": "2.0.2",
|
||||||
"multer": "1.3.0",
|
"pug": "2.0.0-rc.4",
|
||||||
"nprogress": "0.2.0",
|
"ratelimiter": "3.0.3",
|
||||||
"os-utils": "0.0.14",
|
"recaptcha-promise": "0.1.3",
|
||||||
"page": "1.7.1",
|
"reconnecting-websocket": "3.2.2",
|
||||||
"pictograph": "2.0.4",
|
"redis": "2.8.0",
|
||||||
"prominence": "0.2.0",
|
"request": "2.83.0",
|
||||||
"pug": "2.0.0-rc.4",
|
"rimraf": "2.6.2",
|
||||||
"ratelimiter": "3.0.3",
|
"riot": "3.7.4",
|
||||||
"recaptcha-promise": "0.1.3",
|
"riot-tag-loader": "1.0.0",
|
||||||
"reconnecting-websocket": "3.2.2",
|
"rndstr": "1.0.0",
|
||||||
"redis": "2.8.0",
|
"s-age": "1.1.0",
|
||||||
"request": "2.83.0",
|
"seedrandom": "^2.4.3",
|
||||||
"rimraf": "2.6.2",
|
"serve-favicon": "2.4.5",
|
||||||
"riot": "3.7.3",
|
"sortablejs": "1.7.0",
|
||||||
"rndstr": "1.0.0",
|
"string-replace-webpack-plugin": "0.1.3",
|
||||||
"s-age": "1.1.0",
|
"style-loader": "0.19.0",
|
||||||
"serve-favicon": "2.4.5",
|
"stylus": "0.54.5",
|
||||||
"summaly": "2.0.3",
|
"stylus-loader": "3.0.1",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"summaly": "2.0.3",
|
||||||
"tcp-port-used": "0.1.2",
|
"swagger-jsdoc": "1.9.7",
|
||||||
"textarea-caret": "3.0.2",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"ts-node": "3.3.0",
|
"tcp-port-used": "0.1.2",
|
||||||
"typescript": "2.5.3",
|
"textarea-caret": "3.0.2",
|
||||||
"uuid": "3.1.0",
|
"tmp": "0.0.33",
|
||||||
"vhost": "3.0.2",
|
"ts-node": "3.3.0",
|
||||||
"websocket": "1.0.25",
|
"tslint": "5.8.0",
|
||||||
"xev": "2.0.0"
|
"typescript": "2.6.1",
|
||||||
}
|
"uglify-es": "3.1.10",
|
||||||
|
"uglifyjs-webpack-plugin": "1.1.1",
|
||||||
|
"uuid": "3.1.0",
|
||||||
|
"vhost": "3.0.2",
|
||||||
|
"web-push": "3.2.4",
|
||||||
|
"webpack": "3.8.1",
|
||||||
|
"websocket": "1.0.25",
|
||||||
|
"xev": "2.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import User, { IUser, init as initUser } from '../models/user';
|
||||||
|
|
||||||
import getPostSummary from '../../common/get-post-summary';
|
import getPostSummary from '../../common/get-post-summary';
|
||||||
import getUserSummary from '../../common/get-user-summary';
|
import getUserSummary from '../../common/get-user-summary';
|
||||||
|
import getNotificationSummary from '../../common/get-notification-summary';
|
||||||
|
|
||||||
import Othello, { ai as othelloAi } from '../../common/othello';
|
import Othello, { ai as othelloAi } from '../../common/othello';
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ export default class BotCore extends EventEmitter {
|
||||||
return bot;
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async q(query: string): Promise<string | void> {
|
public async q(query: string): Promise<string> {
|
||||||
if (this.context != null) {
|
if (this.context != null) {
|
||||||
return await this.context.q(query);
|
return await this.context.q(query);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +85,10 @@ export default class BotCore extends EventEmitter {
|
||||||
'logout, signout: サインアウトします\n' +
|
'logout, signout: サインアウトします\n' +
|
||||||
'post: 投稿します\n' +
|
'post: 投稿します\n' +
|
||||||
'tl: タイムラインを見ます\n' +
|
'tl: タイムラインを見ます\n' +
|
||||||
'@<ユーザー名>: ユーザーを表示します';
|
'no: 通知を見ます\n' +
|
||||||
|
'@<ユーザー名>: ユーザーを表示します\n' +
|
||||||
|
'\n' +
|
||||||
|
'タイムラインや通知を見た後、「次」というとさらに遡ることができます。';
|
||||||
|
|
||||||
case 'me':
|
case 'me':
|
||||||
return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
|
return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
|
||||||
|
@ -113,7 +117,16 @@ export default class BotCore extends EventEmitter {
|
||||||
|
|
||||||
case 'tl':
|
case 'tl':
|
||||||
case 'タイムライン':
|
case 'タイムライン':
|
||||||
return await this.tlCommand();
|
if (this.user == null) return 'まずサインインしてください。';
|
||||||
|
this.setContext(new TlContext(this));
|
||||||
|
return await this.context.greet();
|
||||||
|
|
||||||
|
case 'no':
|
||||||
|
case 'notifications':
|
||||||
|
case '通知':
|
||||||
|
if (this.user == null) return 'まずサインインしてください。';
|
||||||
|
this.setContext(new NotificationsContext(this));
|
||||||
|
return await this.context.greet();
|
||||||
|
|
||||||
case 'guessing-game':
|
case 'guessing-game':
|
||||||
case '数当てゲーム':
|
case '数当てゲーム':
|
||||||
|
@ -155,21 +168,7 @@ export default class BotCore extends EventEmitter {
|
||||||
this.emit('updated');
|
this.emit('updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async tlCommand(): Promise<string | void> {
|
public async showUserCommand(q: string): Promise<string> {
|
||||||
if (this.user == null) return 'まずサインインしてください。';
|
|
||||||
|
|
||||||
const tl = await require('../endpoints/posts/timeline')({
|
|
||||||
limit: 5
|
|
||||||
}, this.user);
|
|
||||||
|
|
||||||
const text = tl
|
|
||||||
.map(post => getPostSummary(post))
|
|
||||||
.join('\n-----\n');
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async showUserCommand(q: string): Promise<string | void> {
|
|
||||||
try {
|
try {
|
||||||
const user = await require('../endpoints/users/show')({
|
const user = await require('../endpoints/users/show')({
|
||||||
username: q.substr(1)
|
username: q.substr(1)
|
||||||
|
@ -200,6 +199,8 @@ abstract class Context extends EventEmitter {
|
||||||
if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
|
if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
|
||||||
if (data.type == 'othello') return OthelloContext.import(bot, data.content);
|
if (data.type == 'othello') return OthelloContext.import(bot, data.content);
|
||||||
if (data.type == 'post') return PostContext.import(bot, data.content);
|
if (data.type == 'post') return PostContext.import(bot, data.content);
|
||||||
|
if (data.type == 'tl') return TlContext.import(bot, data.content);
|
||||||
|
if (data.type == 'notifications') return NotificationsContext.import(bot, data.content);
|
||||||
if (data.type == 'signin') return SigninContext.import(bot, data.content);
|
if (data.type == 'signin') return SigninContext.import(bot, data.content);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -232,7 +233,7 @@ class SigninContext extends Context {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = bcrypt.compareSync(query, this.temporaryUser.password);
|
const same = await bcrypt.compare(query, this.temporaryUser.password);
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
this.bot.signin(this.temporaryUser);
|
this.bot.signin(this.temporaryUser);
|
||||||
|
@ -285,6 +286,110 @@ class PostContext extends Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TlContext extends Context {
|
||||||
|
private next: string = null;
|
||||||
|
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
return await this.getTl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
if (query == '次') {
|
||||||
|
return await this.getTl();
|
||||||
|
} else {
|
||||||
|
this.bot.clearContext();
|
||||||
|
return await this.bot.q(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTl() {
|
||||||
|
const tl = await require('../endpoints/posts/timeline')({
|
||||||
|
limit: 5,
|
||||||
|
max_id: this.next ? this.next : undefined
|
||||||
|
}, this.bot.user);
|
||||||
|
|
||||||
|
if (tl.length > 0) {
|
||||||
|
this.next = tl[tl.length - 1].id;
|
||||||
|
this.emit('updated');
|
||||||
|
|
||||||
|
const text = tl
|
||||||
|
.map(post => `${post.user.name}\n「${getPostSummary(post)}」`)
|
||||||
|
.join('\n-----\n');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
return 'タイムラインに表示するものがありません...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public export() {
|
||||||
|
return {
|
||||||
|
type: 'tl',
|
||||||
|
content: {
|
||||||
|
next: this.next,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
const context = new TlContext(bot);
|
||||||
|
context.next = data.next;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsContext extends Context {
|
||||||
|
private next: string = null;
|
||||||
|
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
return await this.getNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
if (query == '次') {
|
||||||
|
return await this.getNotifications();
|
||||||
|
} else {
|
||||||
|
this.bot.clearContext();
|
||||||
|
return await this.bot.q(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNotifications() {
|
||||||
|
const notifications = await require('../endpoints/i/notifications')({
|
||||||
|
limit: 5,
|
||||||
|
max_id: this.next ? this.next : undefined
|
||||||
|
}, this.bot.user);
|
||||||
|
|
||||||
|
if (notifications.length > 0) {
|
||||||
|
this.next = notifications[notifications.length - 1].id;
|
||||||
|
this.emit('updated');
|
||||||
|
|
||||||
|
const text = notifications
|
||||||
|
.map(notification => getNotificationSummary(notification))
|
||||||
|
.join('\n-----\n');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
return '通知はありません';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public export() {
|
||||||
|
return {
|
||||||
|
type: 'notifications',
|
||||||
|
content: {
|
||||||
|
next: this.next,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
const context = new NotificationsContext(bot);
|
||||||
|
context.next = data.next;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GuessingGameContext extends Context {
|
class GuessingGameContext extends Context {
|
||||||
private secret: number;
|
private secret: number;
|
||||||
private history: number[] = [];
|
private history: number[] = [];
|
||||||
|
|
|
@ -135,6 +135,8 @@ class LineBot extends BotCore {
|
||||||
actions: actions
|
actions: actions
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showUserTimelinePostback(userId: string) {
|
public async showUserTimelinePostback(userId: string) {
|
||||||
|
|
|
@ -1,172 +1,264 @@
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as tmp from 'tmp';
|
||||||
|
import * as stream from 'stream';
|
||||||
|
|
||||||
import * as mongodb from 'mongodb';
|
import * as mongodb from 'mongodb';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as gm from 'gm';
|
import * as gm from 'gm';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import fileType = require('file-type');
|
import fileType = require('file-type');
|
||||||
import prominence = require('prominence');
|
import prominence = require('prominence');
|
||||||
import DriveFile from '../models/drive-file';
|
|
||||||
|
import DriveFile, { getGridFSBucket } from '../models/drive-file';
|
||||||
import DriveFolder from '../models/drive-folder';
|
import DriveFolder from '../models/drive-folder';
|
||||||
import serialize from '../serializers/drive-file';
|
import serialize from '../serializers/drive-file';
|
||||||
import event from '../event';
|
import event, { publishDriveStream } from '../event';
|
||||||
import config from '../../conf';
|
import config from '../../conf';
|
||||||
|
|
||||||
const log = debug('misskey:register-drive-file');
|
const log = debug('misskey:register-drive-file');
|
||||||
|
|
||||||
|
const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
|
||||||
|
tmp.file((e, path) => {
|
||||||
|
if (e) return reject(e);
|
||||||
|
resolve(path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> =>
|
||||||
|
getGridFSBucket()
|
||||||
|
.then(bucket => new Promise((resolve, reject) => {
|
||||||
|
const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
|
||||||
|
writeStream.once('finish', (doc) => { resolve(doc); });
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
readable.pipe(writeStream);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const addFile = async (
|
||||||
|
user: any,
|
||||||
|
path: string,
|
||||||
|
name: string = null,
|
||||||
|
comment: string = null,
|
||||||
|
folderId: mongodb.ObjectID = null,
|
||||||
|
force: boolean = false
|
||||||
|
) => {
|
||||||
|
log(`registering ${name} (user: ${user.username}, path: ${path})`);
|
||||||
|
|
||||||
|
// Calculate hash, get content type and get file size
|
||||||
|
const [hash, [mime, ext], size] = await Promise.all([
|
||||||
|
// hash
|
||||||
|
((): Promise<string> => new Promise((res, rej) => {
|
||||||
|
const readable = fs.createReadStream(path);
|
||||||
|
const hash = crypto.createHash('md5');
|
||||||
|
const chunks = [];
|
||||||
|
readable
|
||||||
|
.on('error', rej)
|
||||||
|
.pipe(hash)
|
||||||
|
.on('error', rej)
|
||||||
|
.on('data', (chunk) => chunks.push(chunk))
|
||||||
|
.on('end', () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
res(buffer.toString('hex'));
|
||||||
|
});
|
||||||
|
}))(),
|
||||||
|
// mime
|
||||||
|
((): Promise<[string, string | null]> => new Promise((res, rej) => {
|
||||||
|
const readable = fs.createReadStream(path);
|
||||||
|
readable
|
||||||
|
.on('error', rej)
|
||||||
|
.once('data', (buffer: Buffer) => {
|
||||||
|
readable.destroy();
|
||||||
|
const type = fileType(buffer);
|
||||||
|
if (type) {
|
||||||
|
return res([type.mime, type.ext]);
|
||||||
|
} else {
|
||||||
|
// 種類が同定できなかったら application/octet-stream にする
|
||||||
|
return res(['application/octet-stream', null]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}))(),
|
||||||
|
// size
|
||||||
|
((): Promise<number> => new Promise((res, rej) => {
|
||||||
|
fs.stat(path, (err, stats) => {
|
||||||
|
if (err) return rej(err);
|
||||||
|
res(stats.size);
|
||||||
|
});
|
||||||
|
}))()
|
||||||
|
]);
|
||||||
|
|
||||||
|
log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
|
||||||
|
|
||||||
|
// detect name
|
||||||
|
const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
// Check if there is a file with the same hash
|
||||||
|
const much = await DriveFile.findOne({
|
||||||
|
md5: hash,
|
||||||
|
'metadata.user_id': user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (much !== null) {
|
||||||
|
log('file with same hash is found');
|
||||||
|
return much;
|
||||||
|
} else {
|
||||||
|
log('file with same hash is not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [properties, folder] = await Promise.all([
|
||||||
|
// properties
|
||||||
|
(async () => {
|
||||||
|
// 画像かどうか
|
||||||
|
if (!/^image\/.*$/.test(mime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageType = mime.split('/')[1];
|
||||||
|
|
||||||
|
// 画像でもPNGかJPEGでないならスキップ
|
||||||
|
if (imageType != 'png' && imageType != 'jpeg') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file is an image, calculate width and height to save in property
|
||||||
|
const g = gm(fs.createReadStream(path), name);
|
||||||
|
const size = await prominence(g).size();
|
||||||
|
const properties = {
|
||||||
|
width: size.width,
|
||||||
|
height: size.height
|
||||||
|
};
|
||||||
|
|
||||||
|
log('image width and height is calculated');
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
})(),
|
||||||
|
// folder
|
||||||
|
(async () => {
|
||||||
|
if (!folderId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const driveFolder = await DriveFolder.findOne({
|
||||||
|
_id: folderId,
|
||||||
|
user_id: user._id
|
||||||
|
});
|
||||||
|
if (!driveFolder) {
|
||||||
|
throw 'folder-not-found';
|
||||||
|
}
|
||||||
|
return driveFolder;
|
||||||
|
})(),
|
||||||
|
// usage checker
|
||||||
|
(async () => {
|
||||||
|
// Calculate drive usage
|
||||||
|
const usage = await DriveFile
|
||||||
|
.aggregate([{
|
||||||
|
$match: { 'metadata.user_id': user._id }
|
||||||
|
}, {
|
||||||
|
$project: {
|
||||||
|
length: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
usage: { $sum: '$length' }
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
.then((aggregates: any[]) => {
|
||||||
|
if (aggregates.length > 0) {
|
||||||
|
return aggregates[0].usage;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`drive usage is ${usage}`);
|
||||||
|
|
||||||
|
// If usage limit exceeded
|
||||||
|
if (usage + size > user.drive_capacity) {
|
||||||
|
throw 'no-free-space';
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const readable = fs.createReadStream(path);
|
||||||
|
|
||||||
|
return addToGridFS(detectedName, readable, mime, {
|
||||||
|
user_id: user._id,
|
||||||
|
folder_id: folder !== null ? folder._id : null,
|
||||||
|
comment: comment,
|
||||||
|
properties: properties
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add file to drive
|
* Add file to drive
|
||||||
*
|
*
|
||||||
* @param user User who wish to add file
|
* @param user User who wish to add file
|
||||||
* @param fileName File name
|
* @param file File path or readableStream
|
||||||
* @param data Contents
|
|
||||||
* @param comment Comment
|
* @param comment Comment
|
||||||
* @param type File type
|
* @param type File type
|
||||||
* @param folderId Folder ID
|
* @param folderId Folder ID
|
||||||
* @param force If set to true, forcibly upload the file even if there is a file with the same hash.
|
* @param force If set to true, forcibly upload the file even if there is a file with the same hash.
|
||||||
* @return Object that represents added file
|
* @return Object that represents added file
|
||||||
*/
|
*/
|
||||||
export default (
|
export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
|
||||||
user: any,
|
// Get file path
|
||||||
data: Buffer,
|
new Promise((res: (v: [string, boolean]) => void, rej) => {
|
||||||
name: string = null,
|
if (typeof file === 'string') {
|
||||||
comment: string = null,
|
res([file, false]);
|
||||||
folderId: mongodb.ObjectID = null,
|
return;
|
||||||
force: boolean = false
|
|
||||||
) => new Promise<any>(async (resolve, reject) => {
|
|
||||||
log(`registering ${name} (user: ${user.username})`);
|
|
||||||
|
|
||||||
// File size
|
|
||||||
const size = data.byteLength;
|
|
||||||
|
|
||||||
log(`size is ${size}`);
|
|
||||||
|
|
||||||
// File type
|
|
||||||
let mime = 'application/octet-stream';
|
|
||||||
const type = fileType(data);
|
|
||||||
if (type !== null) {
|
|
||||||
mime = type.mime;
|
|
||||||
|
|
||||||
if (name === null) {
|
|
||||||
name = `untitled.${type.ext}`;
|
|
||||||
}
|
}
|
||||||
} else {
|
if (typeof file === 'object' && typeof file.read === 'function') {
|
||||||
if (name === null) {
|
tmpFile()
|
||||||
name = 'untitled';
|
.then(path => {
|
||||||
|
const readable: stream.Readable = file;
|
||||||
|
const writable = fs.createWriteStream(path);
|
||||||
|
readable
|
||||||
|
.on('error', rej)
|
||||||
|
.on('end', () => {
|
||||||
|
res([path, true]);
|
||||||
|
})
|
||||||
|
.pipe(writable)
|
||||||
|
.on('error', rej);
|
||||||
|
})
|
||||||
|
.catch(rej);
|
||||||
}
|
}
|
||||||
}
|
rej(new Error('un-compatible file.'));
|
||||||
|
})
|
||||||
|
.then(([path, remove]): Promise<any> => new Promise((res, rej) => {
|
||||||
|
addFile(user, path, ...args)
|
||||||
|
.then(file => {
|
||||||
|
res(file);
|
||||||
|
if (remove) {
|
||||||
|
fs.unlink(path, (e) => {
|
||||||
|
if (e) log(e.stack);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(rej);
|
||||||
|
}))
|
||||||
|
.then(file => {
|
||||||
|
log(`drive file has been created ${file._id}`);
|
||||||
|
resolve(file);
|
||||||
|
|
||||||
log(`type is ${mime}`);
|
serialize(file).then(serializedFile => {
|
||||||
|
// Publish drive_file_created event
|
||||||
|
event(user._id, 'drive_file_created', serializedFile);
|
||||||
|
publishDriveStream(user._id, 'file_created', serializedFile);
|
||||||
|
|
||||||
// Generate hash
|
// Register to search database
|
||||||
const hash = crypto
|
if (config.elasticsearch.enable) {
|
||||||
.createHash('sha256')
|
const es = require('../../db/elasticsearch');
|
||||||
.update(data)
|
es.index({
|
||||||
.digest('hex') as string;
|
index: 'misskey',
|
||||||
|
type: 'drive_file',
|
||||||
log(`hash is ${hash}`);
|
id: file._id.toString(),
|
||||||
|
body: {
|
||||||
if (!force) {
|
name: file.name,
|
||||||
// Check if there is a file with the same hash
|
user_id: user._id.toString()
|
||||||
const much = await DriveFile.findOne({
|
}
|
||||||
user_id: user._id,
|
});
|
||||||
hash: hash
|
|
||||||
});
|
|
||||||
|
|
||||||
if (much !== null) {
|
|
||||||
log('file with same hash is found');
|
|
||||||
return resolve(much);
|
|
||||||
} else {
|
|
||||||
log('file with same hash is not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate drive usage
|
|
||||||
const usage = ((await DriveFile
|
|
||||||
.aggregate([
|
|
||||||
{ $match: { user_id: user._id } },
|
|
||||||
{ $project: {
|
|
||||||
datasize: true
|
|
||||||
}},
|
|
||||||
{ $group: {
|
|
||||||
_id: null,
|
|
||||||
usage: { $sum: '$datasize' }
|
|
||||||
}}
|
|
||||||
]))[0] || {
|
|
||||||
usage: 0
|
|
||||||
}).usage;
|
|
||||||
|
|
||||||
log(`drive usage is ${usage}`);
|
|
||||||
|
|
||||||
// If usage limit exceeded
|
|
||||||
if (usage + size > user.drive_capacity) {
|
|
||||||
return reject('no-free-space');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the folder is specified
|
|
||||||
let folder: any = null;
|
|
||||||
if (folderId !== null) {
|
|
||||||
folder = await DriveFolder
|
|
||||||
.findOne({
|
|
||||||
_id: folderId,
|
|
||||||
user_id: user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folder === null) {
|
|
||||||
return reject('folder-not-found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let properties: any = null;
|
|
||||||
|
|
||||||
// If the file is an image
|
|
||||||
if (/^image\/.*$/.test(mime)) {
|
|
||||||
// Calculate width and height to save in property
|
|
||||||
const g = gm(data, name);
|
|
||||||
const size = await prominence(g).size();
|
|
||||||
properties = {
|
|
||||||
width: size.width,
|
|
||||||
height: size.height
|
|
||||||
};
|
|
||||||
|
|
||||||
log('image width and height is calculated');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create DriveFile document
|
|
||||||
const file = await DriveFile.insert({
|
|
||||||
created_at: new Date(),
|
|
||||||
user_id: user._id,
|
|
||||||
folder_id: folder !== null ? folder._id : null,
|
|
||||||
data: data,
|
|
||||||
datasize: size,
|
|
||||||
type: mime,
|
|
||||||
name: name,
|
|
||||||
comment: comment,
|
|
||||||
hash: hash,
|
|
||||||
properties: properties
|
|
||||||
});
|
|
||||||
|
|
||||||
delete file.data;
|
|
||||||
|
|
||||||
log(`drive file has been created ${file._id}`);
|
|
||||||
|
|
||||||
resolve(file);
|
|
||||||
|
|
||||||
// Serialize
|
|
||||||
const fileObj = await serialize(file);
|
|
||||||
|
|
||||||
// Publish drive_file_created event
|
|
||||||
event(user._id, 'drive_file_created', fileObj);
|
|
||||||
|
|
||||||
// Register to search database
|
|
||||||
if (config.elasticsearch.enable) {
|
|
||||||
const es = require('../../db/elasticsearch');
|
|
||||||
es.index({
|
|
||||||
index: 'misskey',
|
|
||||||
type: 'drive_file',
|
|
||||||
id: file._id.toString(),
|
|
||||||
body: {
|
|
||||||
name: file.name,
|
|
||||||
user_id: user._id.toString()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,4 +27,12 @@ export default (
|
||||||
// Publish notification event
|
// Publish notification event
|
||||||
event(notifiee, 'notification',
|
event(notifiee, 'notification',
|
||||||
await serialize(notification));
|
await serialize(notification));
|
||||||
|
|
||||||
|
// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
|
setTimeout(async () => {
|
||||||
|
const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
|
||||||
|
if (!fresh.is_read) {
|
||||||
|
event(notifiee, 'unread_notification', await serialize(notification));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
const push = require('web-push');
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import Subscription from '../models/sw-subscription';
|
||||||
|
import config from '../../conf';
|
||||||
|
|
||||||
|
if (config.sw) {
|
||||||
|
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||||
|
push.setVapidDetails(
|
||||||
|
config.maintainer.url,
|
||||||
|
config.sw.public_key,
|
||||||
|
config.sw.private_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function(userId: mongo.ObjectID | string, type, body?) {
|
||||||
|
if (!config.sw) return;
|
||||||
|
|
||||||
|
if (typeof userId === 'string') {
|
||||||
|
userId = new mongo.ObjectID(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
const subscriptions = await Subscription.find({
|
||||||
|
user_id: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriptions.forEach(subscription => {
|
||||||
|
const pushSubscription = {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
auth: subscription.auth,
|
||||||
|
p256dh: subscription.publickey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
push.sendNotification(pushSubscription, JSON.stringify({
|
||||||
|
type, body
|
||||||
|
})).catch(err => {
|
||||||
|
//console.log(err.statusCode);
|
||||||
|
//console.log(err.headers);
|
||||||
|
//console.log(err.body);
|
||||||
|
|
||||||
|
if (err.statusCode == 410) {
|
||||||
|
Subscription.remove({
|
||||||
|
user_id: userId,
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
auth: subscription.auth,
|
||||||
|
publickey: subscription.publickey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import Message from '../models/messaging-message';
|
||||||
import { IMessagingMessage as IMessage } from '../models/messaging-message';
|
import { IMessagingMessage as IMessage } from '../models/messaging-message';
|
||||||
import publishUserStream from '../event';
|
import publishUserStream from '../event';
|
||||||
import { publishMessagingStream } from '../event';
|
import { publishMessagingStream } from '../event';
|
||||||
|
import { publishMessagingIndexStream } from '../event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark as read message(s)
|
* Mark as read message(s)
|
||||||
|
@ -49,6 +50,7 @@ export default (
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
|
publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
|
||||||
|
publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString()));
|
||||||
|
|
||||||
// Calc count of my unread messages
|
// Calc count of my unread messages
|
||||||
const count = await Message
|
const count = await Message
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import config from '../../conf';
|
||||||
|
|
||||||
|
export default function(res, user, redirect: boolean) {
|
||||||
|
const expires = 1000 * 60 * 60 * 24 * 365; // One Year
|
||||||
|
res.cookie('i', user.token, {
|
||||||
|
path: '/',
|
||||||
|
domain: `.${config.host}`,
|
||||||
|
secure: config.url.substr(0, 5) === 'https',
|
||||||
|
httpOnly: false,
|
||||||
|
expires: new Date(Date.now() + expires),
|
||||||
|
maxAge: expires
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
res.redirect(config.url);
|
||||||
|
} else {
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
||||||
|
}
|
|
@ -146,6 +146,11 @@ const endpoints: Endpoint[] = [
|
||||||
name: 'aggregation/posts/reactions'
|
name: 'aggregation/posts/reactions'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'sw/register',
|
||||||
|
withCredential: true
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'i',
|
name: 'i',
|
||||||
withCredential: true
|
withCredential: true
|
||||||
|
@ -159,6 +164,11 @@ const endpoints: Endpoint[] = [
|
||||||
},
|
},
|
||||||
kind: 'account-write'
|
kind: 'account-write'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'i/update_home',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'account-write'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'i/change_password',
|
name: 'i/change_password',
|
||||||
withCredential: true
|
withCredential: true
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||||
if (permissionErr) return rej('invalid permission param');
|
if (permissionErr) return rej('invalid permission param');
|
||||||
|
|
||||||
// Get 'callback_url' parameter
|
// Get 'callback_url' parameter
|
||||||
// TODO: Check $ is valid url
|
// TODO: Check it is valid url
|
||||||
const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$;
|
const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$;
|
||||||
if (callbackUrlErr) return rej('invalid callback_url param');
|
if (callbackUrlErr) return rej('invalid callback_url param');
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import { default as Channel, IChannel } from '../../models/channel';
|
import { default as Channel, IChannel } from '../../models/channel';
|
||||||
import { default as Post, IPost } from '../../models/post';
|
import Post from '../../models/post';
|
||||||
import serialize from '../../serializers/post';
|
import serialize from '../../serializers/post';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Calculate drive usage
|
// Calculate drive usage
|
||||||
const usage = ((await DriveFile
|
const usage = ((await DriveFile
|
||||||
.aggregate([
|
.aggregate([
|
||||||
{ $match: { user_id: user._id } },
|
{ $match: { 'metadata.user_id': user._id } },
|
||||||
{
|
{
|
||||||
$project: {
|
$project: {
|
||||||
datasize: true
|
length: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: null,
|
_id: null,
|
||||||
usage: { $sum: '$datasize' }
|
usage: { $sum: '$length' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]))[0] || {
|
]))[0] || {
|
||||||
|
|
|
@ -13,35 +13,39 @@ import serialize from '../../serializers/drive-file';
|
||||||
* @param {any} app
|
* @param {any} app
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
module.exports = async (params, user, app) => {
|
||||||
// Get 'limit' parameter
|
// Get 'limit' parameter
|
||||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||||
if (limitErr) return rej('invalid limit param');
|
if (limitErr) throw 'invalid limit param';
|
||||||
|
|
||||||
// Get 'since_id' parameter
|
// Get 'since_id' parameter
|
||||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||||
if (sinceIdErr) return rej('invalid since_id param');
|
if (sinceIdErr) throw 'invalid since_id param';
|
||||||
|
|
||||||
// Get 'max_id' parameter
|
// Get 'max_id' parameter
|
||||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||||
if (maxIdErr) return rej('invalid max_id param');
|
if (maxIdErr) throw 'invalid max_id param';
|
||||||
|
|
||||||
// Check if both of since_id and max_id is specified
|
// Check if both of since_id and max_id is specified
|
||||||
if (sinceId && maxId) {
|
if (sinceId && maxId) {
|
||||||
return rej('cannot set since_id and max_id');
|
throw 'cannot set since_id and max_id';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 'folder_id' parameter
|
// Get 'folder_id' parameter
|
||||||
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||||
if (folderIdErr) return rej('invalid folder_id param');
|
if (folderIdErr) throw 'invalid folder_id param';
|
||||||
|
|
||||||
|
// Get 'type' parameter
|
||||||
|
const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$;
|
||||||
|
if (typeErr) throw 'invalid type param';
|
||||||
|
|
||||||
// Construct query
|
// Construct query
|
||||||
const sort = {
|
const sort = {
|
||||||
_id: -1
|
_id: -1
|
||||||
};
|
};
|
||||||
const query = {
|
const query = {
|
||||||
user_id: user._id,
|
'metadata.user_id': user._id,
|
||||||
folder_id: folderId
|
'metadata.folder_id': folderId
|
||||||
} as any;
|
} as any;
|
||||||
if (sinceId) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
|
@ -53,18 +57,18 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
$lt: maxId
|
$lt: maxId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (type) {
|
||||||
|
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||||
|
}
|
||||||
|
|
||||||
// Issue query
|
// Issue query
|
||||||
const files = await DriveFile
|
const files = await DriveFile
|
||||||
.find(query, {
|
.find(query, {
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
},
|
|
||||||
limit: limit,
|
limit: limit,
|
||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await Promise.all(files.map(async file =>
|
const _files = await Promise.all(files.map(file => serialize(file)));
|
||||||
await serialize(file))));
|
return _files;
|
||||||
});
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import * as fs from 'fs';
|
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import { validateFileName } from '../../../models/drive-file';
|
import { validateFileName } from '../../../models/drive-file';
|
||||||
import serialize from '../../../serializers/drive-file';
|
import serialize from '../../../serializers/drive-file';
|
||||||
|
@ -15,14 +14,11 @@ import create from '../../../common/add-file-to-drive';
|
||||||
* @param {any} user
|
* @param {any} user
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
module.exports = async (file, params, user): Promise<any> => {
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
return rej('file is required');
|
throw 'file is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = fs.readFileSync(file.path);
|
|
||||||
fs.unlink(file.path, (err) => { if (err) console.log(err); });
|
|
||||||
|
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
let name = file.originalname;
|
let name = file.originalname;
|
||||||
if (name !== undefined && name !== null) {
|
if (name !== undefined && name !== null) {
|
||||||
|
@ -32,7 +28,7 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
||||||
} else if (name === 'blob') {
|
} else if (name === 'blob') {
|
||||||
name = null;
|
name = null;
|
||||||
} else if (!validateFileName(name)) {
|
} else if (!validateFileName(name)) {
|
||||||
return rej('invalid name');
|
throw 'invalid name';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
name = null;
|
name = null;
|
||||||
|
@ -40,14 +36,11 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
// Get 'folder_id' parameter
|
// Get 'folder_id' parameter
|
||||||
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||||
if (folderIdErr) return rej('invalid folder_id param');
|
if (folderIdErr) throw 'invalid folder_id param';
|
||||||
|
|
||||||
// Create file
|
// Create file
|
||||||
const driveFile = await create(user, buffer, name, null, folderId);
|
const driveFile = await create(user, file.path, name, null, folderId);
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
const fileObj = await serialize(driveFile);
|
return serialize(driveFile);
|
||||||
|
};
|
||||||
// Response
|
|
||||||
res(fileObj);
|
|
||||||
});
|
|
||||||
|
|
|
@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Issue query
|
// Issue query
|
||||||
const files = await DriveFile
|
const files = await DriveFile
|
||||||
.find({
|
.find({
|
||||||
name: name,
|
filename: name,
|
||||||
user_id: user._id,
|
'metadata.user_id': user._id,
|
||||||
folder_id: folderId
|
'metadata.folder_id': folderId
|
||||||
}, {
|
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
|
|
|
@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file';
|
||||||
* @param {any} user
|
* @param {any} user
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
module.exports = async (params, user) => {
|
||||||
// Get 'file_id' parameter
|
// Get 'file_id' parameter
|
||||||
const [fileId, fileIdErr] = $(params.file_id).id().$;
|
const [fileId, fileIdErr] = $(params.file_id).id().$;
|
||||||
if (fileIdErr) return rej('invalid file_id param');
|
if (fileIdErr) throw 'invalid file_id param';
|
||||||
|
|
||||||
// Fetch file
|
// Fetch file
|
||||||
const file = await DriveFile
|
const file = await DriveFile
|
||||||
.findOne({
|
.findOne({
|
||||||
_id: fileId,
|
_id: fileId,
|
||||||
user_id: user._id
|
'metadata.user_id': user._id
|
||||||
}, {
|
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
return rej('file-not-found');
|
throw 'file-not-found';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await serialize(file, {
|
const _file = await serialize(file, {
|
||||||
detail: true
|
detail: true
|
||||||
}));
|
});
|
||||||
});
|
|
||||||
|
return _file;
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import DriveFolder from '../../../models/drive-folder';
|
||||||
import DriveFile from '../../../models/drive-file';
|
import DriveFile from '../../../models/drive-file';
|
||||||
import { validateFileName } from '../../../models/drive-file';
|
import { validateFileName } from '../../../models/drive-file';
|
||||||
import serialize from '../../../serializers/drive-file';
|
import serialize from '../../../serializers/drive-file';
|
||||||
import event from '../../../event';
|
import { publishDriveStream } from '../../../event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a file
|
* Update a file
|
||||||
|
@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
const file = await DriveFile
|
const file = await DriveFile
|
||||||
.findOne({
|
.findOne({
|
||||||
_id: fileId,
|
_id: fileId,
|
||||||
user_id: user._id
|
'metadata.user_id': user._id
|
||||||
}, {
|
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
|
@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
|
const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
|
||||||
if (nameErr) return rej('invalid name param');
|
if (nameErr) return rej('invalid name param');
|
||||||
if (name) file.name = name;
|
if (name) file.filename = name;
|
||||||
|
|
||||||
// Get 'folder_id' parameter
|
// Get 'folder_id' parameter
|
||||||
const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||||
|
@ -46,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
if (folderId !== undefined) {
|
if (folderId !== undefined) {
|
||||||
if (folderId === null) {
|
if (folderId === null) {
|
||||||
file.folder_id = null;
|
file.metadata.folder_id = null;
|
||||||
} else {
|
} else {
|
||||||
// Fetch folder
|
// Fetch folder
|
||||||
const folder = await DriveFolder
|
const folder = await DriveFolder
|
||||||
|
@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
return rej('folder-not-found');
|
return rej('folder-not-found');
|
||||||
}
|
}
|
||||||
|
|
||||||
file.folder_id = folder._id;
|
file.metadata.folder_id = folder._id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DriveFile.update(file._id, {
|
await DriveFile.update(file._id, {
|
||||||
$set: {
|
$set: {
|
||||||
name: file.name,
|
filename: file.filename,
|
||||||
folder_id: file.folder_id
|
'metadata.folder_id': file.metadata.folder_id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,6 +72,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Response
|
// Response
|
||||||
res(fileObj);
|
res(fileObj);
|
||||||
|
|
||||||
// Publish drive_file_updated event
|
// Publish file_updated event
|
||||||
event(user._id, 'drive_file_updated', fileObj);
|
publishDriveStream(user._id, 'file_updated', fileObj);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,16 @@
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import * as URL from 'url';
|
import * as URL from 'url';
|
||||||
const download = require('download');
|
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import { validateFileName } from '../../../models/drive-file';
|
import { validateFileName } from '../../../models/drive-file';
|
||||||
import serialize from '../../../serializers/drive-file';
|
import serialize from '../../../serializers/drive-file';
|
||||||
import create from '../../../common/add-file-to-drive';
|
import create from '../../../common/add-file-to-drive';
|
||||||
|
import * as debug from 'debug';
|
||||||
|
import * as tmp from 'tmp';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as request from 'request';
|
||||||
|
|
||||||
|
const log = debug('misskey:endpoint:upload_from_url');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a file from a URL
|
* Create a file from a URL
|
||||||
|
@ -15,11 +20,11 @@ import create from '../../../common/add-file-to-drive';
|
||||||
* @param {any} user
|
* @param {any} user
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
module.exports = async (params, user): Promise<any> => {
|
||||||
// Get 'url' parameter
|
// Get 'url' parameter
|
||||||
// TODO: Validate this url
|
// TODO: Validate this url
|
||||||
const [url, urlErr] = $(params.url).string().$;
|
const [url, urlErr] = $(params.url).string().$;
|
||||||
if (urlErr) return rej('invalid url param');
|
if (urlErr) throw 'invalid url param';
|
||||||
|
|
||||||
let name = URL.parse(url).pathname.split('/').pop();
|
let name = URL.parse(url).pathname.split('/').pop();
|
||||||
if (!validateFileName(name)) {
|
if (!validateFileName(name)) {
|
||||||
|
@ -28,17 +33,35 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
// Get 'folder_id' parameter
|
// Get 'folder_id' parameter
|
||||||
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
|
||||||
if (folderIdErr) return rej('invalid folder_id param');
|
if (folderIdErr) throw 'invalid folder_id param';
|
||||||
|
|
||||||
// Download file
|
// Create temp file
|
||||||
const data = await download(url);
|
const path = await new Promise((res: (string) => void, rej) => {
|
||||||
|
tmp.file((e, path) => {
|
||||||
|
if (e) return rej(e);
|
||||||
|
res(path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Create file
|
// write content at URL to temp file
|
||||||
const driveFile = await create(user, data, name, null, folderId);
|
await new Promise((res, rej) => {
|
||||||
|
const writable = fs.createWriteStream(path);
|
||||||
|
request(url)
|
||||||
|
.on('error', rej)
|
||||||
|
.on('end', () => {
|
||||||
|
writable.close();
|
||||||
|
res(path);
|
||||||
|
})
|
||||||
|
.pipe(writable)
|
||||||
|
.on('error', rej);
|
||||||
|
});
|
||||||
|
|
||||||
// Serialize
|
const driveFile = await create(user, path, name, null, folderId);
|
||||||
const fileObj = await serialize(driveFile);
|
|
||||||
|
|
||||||
// Response
|
// clean-up
|
||||||
res(fileObj);
|
fs.unlink(path, (e) => {
|
||||||
});
|
if (e) log(e.stack);
|
||||||
|
});
|
||||||
|
|
||||||
|
return serialize(driveFile);
|
||||||
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import $ from 'cafy';
|
||||||
import DriveFolder from '../../../models/drive-folder';
|
import DriveFolder from '../../../models/drive-folder';
|
||||||
import { isValidFolderName } from '../../../models/drive-folder';
|
import { isValidFolderName } from '../../../models/drive-folder';
|
||||||
import serialize from '../../../serializers/drive-folder';
|
import serialize from '../../../serializers/drive-folder';
|
||||||
import event from '../../../event';
|
import { publishDriveStream } from '../../../event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create drive folder
|
* Create drive folder
|
||||||
|
@ -52,6 +52,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Response
|
// Response
|
||||||
res(folderObj);
|
res(folderObj);
|
||||||
|
|
||||||
// Publish drive_folder_created event
|
// Publish folder_created event
|
||||||
event(user._id, 'drive_folder_created', folderObj);
|
publishDriveStream(user._id, 'folder_created', folderObj);
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await Promise.all(folders.map(async folder =>
|
res(await Promise.all(folders.map(folder => serialize(folder))));
|
||||||
await serialize(folder))));
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import DriveFolder from '../../../models/drive-folder';
|
import DriveFolder from '../../../models/drive-folder';
|
||||||
import { isValidFolderName } from '../../../models/drive-folder';
|
import { isValidFolderName } from '../../../models/drive-folder';
|
||||||
import serialize from '../../../serializers/drive-file';
|
import serialize from '../../../serializers/drive-folder';
|
||||||
import event from '../../../event';
|
import { publishDriveStream } from '../../../event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a folder
|
* Update a folder
|
||||||
|
@ -96,6 +96,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Response
|
// Response
|
||||||
res(folderObj);
|
res(folderObj);
|
||||||
|
|
||||||
// Publish drive_folder_updated event
|
// Publish folder_updated event
|
||||||
event(user._id, 'drive_folder_updated', folderObj);
|
publishDriveStream(user._id, 'folder_updated', folderObj);
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
_id: -1
|
_id: -1
|
||||||
};
|
};
|
||||||
const query = {
|
const query = {
|
||||||
user_id: user._id
|
'metadata.user_id': user._id
|
||||||
} as any;
|
} as any;
|
||||||
if (sinceId) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
|
@ -52,15 +52,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type) {
|
if (type) {
|
||||||
query.type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue query
|
// Issue query
|
||||||
const files = await DriveFile
|
const files = await DriveFile
|
||||||
.find(query, {
|
.find(query, {
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
},
|
|
||||||
limit: limit,
|
limit: limit,
|
||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,38 +13,27 @@ import Appdata from '../../../models/appdata';
|
||||||
* @param {Boolean} isSecure
|
* @param {Boolean} isSecure
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => {
|
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
|
if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
|
||||||
|
|
||||||
// Get 'key' parameter
|
// Get 'key' parameter
|
||||||
const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$;
|
const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$;
|
||||||
if (keyError) return rej('invalid key param');
|
if (keyError) return rej('invalid key param');
|
||||||
|
|
||||||
if (isSecure) {
|
const select = {};
|
||||||
if (!user.data) {
|
if (key !== null) {
|
||||||
return res();
|
select[`data.${key}`] = true;
|
||||||
}
|
}
|
||||||
if (key !== null) {
|
const appdata = await Appdata.findOne({
|
||||||
const data = {};
|
app_id: app._id,
|
||||||
data[key] = user.data[key];
|
user_id: user._id
|
||||||
res(data);
|
}, {
|
||||||
} else {
|
fields: select
|
||||||
res(user.data);
|
});
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const select = {};
|
|
||||||
if (key !== null) {
|
|
||||||
select[`data.${key}`] = true;
|
|
||||||
}
|
|
||||||
const appdata = await Appdata.findOne({
|
|
||||||
app_id: app._id,
|
|
||||||
user_id: user._id
|
|
||||||
}, {
|
|
||||||
fields: select
|
|
||||||
});
|
|
||||||
|
|
||||||
if (appdata) {
|
if (appdata) {
|
||||||
res(appdata.data);
|
res(appdata.data);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,6 @@
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import Appdata from '../../../models/appdata';
|
import Appdata from '../../../models/appdata';
|
||||||
import User from '../../../models/user';
|
|
||||||
import serialize from '../../../serializers/user';
|
|
||||||
import event from '../../../event';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set app data
|
* Set app data
|
||||||
|
@ -16,7 +13,9 @@ import event from '../../../event';
|
||||||
* @param {Boolean} isSecure
|
* @param {Boolean} isSecure
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => {
|
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
|
if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
|
||||||
|
|
||||||
// Get 'data' parameter
|
// Get 'data' parameter
|
||||||
const [data, dataError] = $(params.data).optional.object()
|
const [data, dataError] = $(params.data).optional.object()
|
||||||
.pipe(obj => {
|
.pipe(obj => {
|
||||||
|
@ -43,31 +42,17 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) =
|
||||||
set[`data.${key}`] = value;
|
set[`data.${key}`] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSecure) {
|
await Appdata.update({
|
||||||
const _user = await User.findOneAndUpdate(user._id, {
|
app_id: app._id,
|
||||||
|
user_id: user._id
|
||||||
|
}, Object.assign({
|
||||||
|
app_id: app._id,
|
||||||
|
user_id: user._id
|
||||||
|
}, {
|
||||||
$set: set
|
$set: set
|
||||||
|
}), {
|
||||||
|
upsert: true
|
||||||
});
|
});
|
||||||
|
|
||||||
res(204);
|
res(204);
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
event(user._id, 'i_updated', await serialize(_user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
await Appdata.update({
|
|
||||||
app_id: app._id,
|
|
||||||
user_id: user._id
|
|
||||||
}, Object.assign({
|
|
||||||
app_id: app._id,
|
|
||||||
user_id: user._id
|
|
||||||
}, {
|
|
||||||
$set: set
|
|
||||||
}), {
|
|
||||||
upsert: true
|
|
||||||
});
|
|
||||||
|
|
||||||
res(204);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,15 +22,15 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||||
if (newPasswordErr) return rej('invalid new_password param');
|
if (newPasswordErr) return rej('invalid new_password param');
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = bcrypt.compareSync(currentPassword, user.password);
|
const same = await bcrypt.compare(currentPassword, user.password);
|
||||||
|
|
||||||
if (!same) {
|
if (!same) {
|
||||||
return rej('incorrect password');
|
return rej('incorrect password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = bcrypt.genSaltSync(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
const hash = bcrypt.hashSync(newPassword, salt);
|
const hash = await bcrypt.hash(newPassword, salt);
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
$set: {
|
$set: {
|
||||||
|
|
|
@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||||
if (passwordErr) return rej('invalid password param');
|
if (passwordErr) return rej('invalid password param');
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = bcrypt.compareSync(password, user.password);
|
const same = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
if (!same) {
|
if (!same) {
|
||||||
return rej('incorrect password');
|
return rej('incorrect password');
|
||||||
|
|
|
@ -48,13 +48,19 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
|
||||||
if (bannerIdErr) return rej('invalid banner_id param');
|
if (bannerIdErr) return rej('invalid banner_id param');
|
||||||
if (bannerId) user.banner_id = bannerId;
|
if (bannerId) user.banner_id = bannerId;
|
||||||
|
|
||||||
|
// Get 'show_donation' parameter
|
||||||
|
const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$;
|
||||||
|
if (showDonationErr) return rej('invalid show_donation param');
|
||||||
|
if (showDonation) user.client_settings.show_donation = showDonation;
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
$set: {
|
$set: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
description: user.description,
|
description: user.description,
|
||||||
avatar_id: user.avatar_id,
|
avatar_id: user.avatar_id,
|
||||||
banner_id: user.banner_id,
|
banner_id: user.banner_id,
|
||||||
profile: user.profile
|
profile: user.profile,
|
||||||
|
'client_settings.show_donation': user.client_settings.show_donation
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
import $ from 'cafy';
|
||||||
|
import User from '../../models/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update myself
|
||||||
|
*
|
||||||
|
* @param {any} params
|
||||||
|
* @param {any} user
|
||||||
|
* @param {any} _
|
||||||
|
* @param {boolean} isSecure
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'home' parameter
|
||||||
|
const [home, homeErr] = $(params.home).optional.array().each(
|
||||||
|
$().strict.object()
|
||||||
|
.have('name', $().string())
|
||||||
|
.have('id', $().string())
|
||||||
|
.have('place', $().string())
|
||||||
|
.have('data', $().object())).$;
|
||||||
|
if (homeErr) return rej('invalid home param');
|
||||||
|
|
||||||
|
// Get 'id' parameter
|
||||||
|
const [id, idErr] = $(params.id).optional.string().$;
|
||||||
|
if (idErr) return rej('invalid id param');
|
||||||
|
|
||||||
|
// Get 'data' parameter
|
||||||
|
const [data, dataErr] = $(params.data).optional.object().$;
|
||||||
|
if (dataErr) return rej('invalid data param');
|
||||||
|
|
||||||
|
if (home) {
|
||||||
|
await User.update(user._id, {
|
||||||
|
$set: {
|
||||||
|
'client_settings.home': home
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
} else {
|
||||||
|
if (id == null && data == null) return rej('you need to set id and data params if home param unset');
|
||||||
|
|
||||||
|
const _home = user.client_settings.home;
|
||||||
|
const widget = _home.find(w => w.id == id);
|
||||||
|
|
||||||
|
if (widget == null) return rej('widget not found');
|
||||||
|
|
||||||
|
widget.data = data;
|
||||||
|
|
||||||
|
await User.update(user._id, {
|
||||||
|
$set: {
|
||||||
|
'client_settings.home': _home
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
|
@ -9,7 +9,7 @@ import User from '../../../models/user';
|
||||||
import DriveFile from '../../../models/drive-file';
|
import DriveFile from '../../../models/drive-file';
|
||||||
import serialize from '../../../serializers/messaging-message';
|
import serialize from '../../../serializers/messaging-message';
|
||||||
import publishUserStream from '../../../event';
|
import publishUserStream from '../../../event';
|
||||||
import { publishMessagingStream } from '../../../event';
|
import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
|
||||||
import config from '../../../../conf';
|
import config from '../../../../conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
if (fileId !== undefined) {
|
if (fileId !== undefined) {
|
||||||
file = await DriveFile.findOne({
|
file = await DriveFile.findOne({
|
||||||
_id: fileId,
|
_id: fileId,
|
||||||
user_id: user._id
|
'metadata.user_id': user._id
|
||||||
}, {
|
|
||||||
data: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
|
@ -87,10 +85,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
// 自分のストリーム
|
// 自分のストリーム
|
||||||
publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
|
publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
|
||||||
|
publishMessagingIndexStream(message.user_id, 'message', messageObj);
|
||||||
publishUserStream(message.user_id, 'messaging_message', messageObj);
|
publishUserStream(message.user_id, 'messaging_message', messageObj);
|
||||||
|
|
||||||
// 相手のストリーム
|
// 相手のストリーム
|
||||||
publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
|
publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
|
||||||
|
publishMessagingIndexStream(message.recipient_id, 'message', messageObj);
|
||||||
publishUserStream(message.recipient_id, 'messaging_message', messageObj);
|
publishUserStream(message.recipient_id, 'messaging_message', messageObj);
|
||||||
|
|
||||||
// 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
|
// 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
|
||||||
|
@ -98,6 +98,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
|
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
|
||||||
if (!freshMessage.is_read) {
|
if (!freshMessage.is_read) {
|
||||||
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
|
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||||
|
pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import version from '../../version';
|
import version from '../../version';
|
||||||
import config from '../../conf';
|
import config from '../../conf';
|
||||||
|
import Meta from '../models/meta';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
|
@ -39,6 +40,8 @@ import config from '../../conf';
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params) => new Promise(async (res, rej) => {
|
module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
|
const meta = (await Meta.findOne()) || {};
|
||||||
|
|
||||||
res({
|
res({
|
||||||
maintainer: config.maintainer,
|
maintainer: config.maintainer,
|
||||||
version: version,
|
version: version,
|
||||||
|
@ -49,6 +52,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
||||||
cpu: {
|
cpu: {
|
||||||
model: os.cpus()[0].model,
|
model: os.cpus()[0].model,
|
||||||
cores: os.cpus().length
|
cores: os.cpus().length
|
||||||
}
|
},
|
||||||
|
top_image: meta.top_image,
|
||||||
|
broadcasts: meta.broadcasts
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ import ChannelWatching from '../../models/channel-watching';
|
||||||
import serialize from '../../serializers/post';
|
import serialize from '../../serializers/post';
|
||||||
import notify from '../../common/notify';
|
import notify from '../../common/notify';
|
||||||
import watch from '../../common/watch-post';
|
import watch from '../../common/watch-post';
|
||||||
import { default as event, publishChannelStream } from '../../event';
|
import event, { pushSw, publishChannelStream } from '../../event';
|
||||||
import config from '../../../conf';
|
import config from '../../../conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
// SELECT _id
|
// SELECT _id
|
||||||
const entity = await DriveFile.findOne({
|
const entity = await DriveFile.findOne({
|
||||||
_id: mediaId,
|
_id: mediaId,
|
||||||
user_id: user._id
|
'metadata.user_id': user._id
|
||||||
}, {
|
|
||||||
_id: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entity === null) {
|
if (entity === null) {
|
||||||
|
@ -236,7 +234,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
const mentions = [];
|
const mentions = [];
|
||||||
|
|
||||||
function addMention(mentionee, type) {
|
function addMention(mentionee, reason) {
|
||||||
// Reject if already added
|
// Reject if already added
|
||||||
if (mentions.some(x => x.equals(mentionee))) return;
|
if (mentions.some(x => x.equals(mentionee))) return;
|
||||||
|
|
||||||
|
@ -245,7 +243,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
if (!user._id.equals(mentionee)) {
|
if (!user._id.equals(mentionee)) {
|
||||||
event(mentionee, type, postObj);
|
event(mentionee, reason, postObj);
|
||||||
|
pushSw(mentionee, reason, postObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,9 @@ import Post from '../../../models/post';
|
||||||
import Watching from '../../../models/post-watching';
|
import Watching from '../../../models/post-watching';
|
||||||
import notify from '../../../common/notify';
|
import notify from '../../../common/notify';
|
||||||
import watch from '../../../common/watch-post';
|
import watch from '../../../common/watch-post';
|
||||||
import { publishPostStream } from '../../../event';
|
import { publishPostStream, pushSw } from '../../../event';
|
||||||
|
import serializePost from '../../../serializers/post';
|
||||||
|
import serializeUser from '../../../serializers/user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React to a post
|
* React to a post
|
||||||
|
@ -87,6 +89,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
reaction: reaction
|
reaction: reaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pushSw(post.user_id, 'reaction', {
|
||||||
|
user: await serializeUser(user, post.user_id),
|
||||||
|
post: await serializePost(post, post.user_id),
|
||||||
|
reaction: reaction
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch watchers
|
// Fetch watchers
|
||||||
Watching
|
Watching
|
||||||
.find({
|
.find({
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
|
import rap from '@prezzemolo/rap';
|
||||||
import Post from '../../models/post';
|
import Post from '../../models/post';
|
||||||
import ChannelWatching from '../../models/channel-watching';
|
import ChannelWatching from '../../models/channel-watching';
|
||||||
import getFriends from '../../common/get-friends';
|
import getFriends from '../../common/get-friends';
|
||||||
|
@ -15,32 +16,41 @@ import serialize from '../../serializers/post';
|
||||||
* @param {any} app
|
* @param {any} app
|
||||||
* @return {Promise<any>}
|
* @return {Promise<any>}
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
module.exports = async (params, user, app) => {
|
||||||
// Get 'limit' parameter
|
// Get 'limit' parameter
|
||||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||||
if (limitErr) return rej('invalid limit param');
|
if (limitErr) throw 'invalid limit param';
|
||||||
|
|
||||||
// Get 'since_id' parameter
|
// Get 'since_id' parameter
|
||||||
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
|
||||||
if (sinceIdErr) return rej('invalid since_id param');
|
if (sinceIdErr) throw 'invalid since_id param';
|
||||||
|
|
||||||
// Get 'max_id' parameter
|
// Get 'max_id' parameter
|
||||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||||
if (maxIdErr) return rej('invalid max_id param');
|
if (maxIdErr) throw 'invalid max_id param';
|
||||||
|
|
||||||
// Check if both of since_id and max_id is specified
|
// Get 'since_date' parameter
|
||||||
if (sinceId && maxId) {
|
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
|
||||||
return rej('cannot set since_id and max_id');
|
if (sinceDateErr) throw 'invalid since_date param';
|
||||||
|
|
||||||
|
// Get 'max_date' parameter
|
||||||
|
const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
|
||||||
|
if (maxDateErr) throw 'invalid max_date param';
|
||||||
|
|
||||||
|
// Check if only one of since_id, max_id, since_date, max_date specified
|
||||||
|
if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
|
||||||
|
throw 'only one of since_id, max_id, since_date, max_date can be specified';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID list of the user itself and other users who the user follows
|
const { followingIds, watchingChannelIds } = await rap({
|
||||||
const followingIds = await getFriends(user._id);
|
// ID list of the user itself and other users who the user follows
|
||||||
|
followingIds: getFriends(user._id),
|
||||||
// Watchしているチャンネルを取得
|
// Watchしているチャンネルを取得
|
||||||
const watches = await ChannelWatching.find({
|
watchingChannelIds: ChannelWatching.find({
|
||||||
user_id: user._id,
|
user_id: user._id,
|
||||||
// 削除されたドキュメントは除く
|
// 削除されたドキュメントは除く
|
||||||
deleted_at: { $exists: false }
|
deleted_at: { $exists: false }
|
||||||
|
}).then(watches => watches.map(w => w.channel_id))
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
|
@ -65,7 +75,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
}, {
|
}, {
|
||||||
// Watchしているチャンネルへの投稿
|
// Watchしているチャンネルへの投稿
|
||||||
channel_id: {
|
channel_id: {
|
||||||
$in: watches.map(w => w.channel_id)
|
$in: watchingChannelIds
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
} as any;
|
} as any;
|
||||||
|
@ -79,6 +89,15 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
query._id = {
|
query._id = {
|
||||||
$lt: maxId
|
$lt: maxId
|
||||||
};
|
};
|
||||||
|
} else if (sinceDate) {
|
||||||
|
sort._id = 1;
|
||||||
|
query.created_at = {
|
||||||
|
$gt: new Date(sinceDate)
|
||||||
|
};
|
||||||
|
} else if (maxDate) {
|
||||||
|
query.created_at = {
|
||||||
|
$lt: new Date(maxDate)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -90,7 +109,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await Promise.all(timeline.map(async post =>
|
return await Promise.all(timeline.map(post => serialize(post, user)));
|
||||||
await serialize(post, user)
|
};
|
||||||
)));
|
|
||||||
});
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies
|
||||||
|
*/
|
||||||
|
import $ from 'cafy';
|
||||||
|
import Subscription from '../../models/sw-subscription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* subscribe service worker
|
||||||
|
*
|
||||||
|
* @param {any} params
|
||||||
|
* @param {any} user
|
||||||
|
* @param {any} _
|
||||||
|
* @param {boolean} isSecure
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'endpoint' parameter
|
||||||
|
const [endpoint, endpointErr] = $(params.endpoint).string().$;
|
||||||
|
if (endpointErr) return rej('invalid endpoint param');
|
||||||
|
|
||||||
|
// Get 'auth' parameter
|
||||||
|
const [auth, authErr] = $(params.auth).string().$;
|
||||||
|
if (authErr) return rej('invalid auth param');
|
||||||
|
|
||||||
|
// Get 'publickey' parameter
|
||||||
|
const [publickey, publickeyErr] = $(params.publickey).string().$;
|
||||||
|
if (publickeyErr) return rej('invalid publickey param');
|
||||||
|
|
||||||
|
// if already subscribed
|
||||||
|
const exist = await Subscription.findOne({
|
||||||
|
user_id: user._id,
|
||||||
|
endpoint: endpoint,
|
||||||
|
auth: auth,
|
||||||
|
publickey: publickey,
|
||||||
|
deleted_at: { $exists: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist !== null) {
|
||||||
|
return res();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Subscription.insert({
|
||||||
|
user_id: user._id,
|
||||||
|
endpoint: endpoint,
|
||||||
|
auth: auth,
|
||||||
|
publickey: publickey
|
||||||
|
});
|
||||||
|
|
||||||
|
res();
|
||||||
|
});
|
|
@ -11,6 +11,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
const [userId, userIdErr] = $(params.user_id).id().$;
|
const [userId, userIdErr] = $(params.user_id).id().$;
|
||||||
if (userIdErr) return rej('invalid user_id param');
|
if (userIdErr) return rej('invalid user_id param');
|
||||||
|
|
||||||
|
// Get 'limit' parameter
|
||||||
|
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||||
|
if (limitErr) return rej('invalid limit param');
|
||||||
|
|
||||||
// Lookup user
|
// Lookup user
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
_id: userId
|
_id: userId
|
||||||
|
@ -82,8 +86,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
// Sort replies by frequency
|
// Sort replies by frequency
|
||||||
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
|
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
|
||||||
|
|
||||||
// Lookup top 10 replies
|
// Extract top replied users
|
||||||
const topRepliedUsers = repliedUsersSorted.slice(0, 10);
|
const topRepliedUsers = repliedUsersSorted.slice(0, limit);
|
||||||
|
|
||||||
// Make replies object (includes weights)
|
// Make replies object (includes weights)
|
||||||
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
|
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
|
||||||
|
|
|
@ -46,9 +46,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
|
||||||
if (maxIdErr) return rej('invalid max_id param');
|
if (maxIdErr) return rej('invalid max_id param');
|
||||||
|
|
||||||
// Check if both of since_id and max_id is specified
|
// Get 'since_date' parameter
|
||||||
if (sinceId && maxId) {
|
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
|
||||||
return rej('cannot set since_id and max_id');
|
if (sinceDateErr) throw 'invalid since_date param';
|
||||||
|
|
||||||
|
// Get 'max_date' parameter
|
||||||
|
const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
|
||||||
|
if (maxDateErr) throw 'invalid max_date param';
|
||||||
|
|
||||||
|
// Check if only one of since_id, max_id, since_date, max_date specified
|
||||||
|
if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
|
||||||
|
throw 'only one of since_id, max_id, since_date, max_date can be specified';
|
||||||
}
|
}
|
||||||
|
|
||||||
const q = userId !== undefined
|
const q = userId !== undefined
|
||||||
|
@ -66,13 +74,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
return rej('user not found');
|
return rej('user not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct query
|
//#region Construct query
|
||||||
const sort = {
|
const sort = {
|
||||||
_id: -1
|
_id: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
user_id: user._id
|
user_id: user._id
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (sinceId) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
query._id = {
|
query._id = {
|
||||||
|
@ -82,6 +92,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
query._id = {
|
query._id = {
|
||||||
$lt: maxId
|
$lt: maxId
|
||||||
};
|
};
|
||||||
|
} else if (sinceDate) {
|
||||||
|
sort._id = 1;
|
||||||
|
query.created_at = {
|
||||||
|
$gt: new Date(sinceDate)
|
||||||
|
};
|
||||||
|
} else if (maxDate) {
|
||||||
|
query.created_at = {
|
||||||
|
$lt: new Date(maxDate)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!includeReplies) {
|
if (!includeReplies) {
|
||||||
|
@ -94,6 +113,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
$ne: null
|
$ne: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// Issue query
|
// Issue query
|
||||||
const posts = await Post
|
const posts = await Post
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as mongo from 'mongodb';
|
import * as mongo from 'mongodb';
|
||||||
import * as redis from 'redis';
|
import * as redis from 'redis';
|
||||||
|
import swPush from './common/push-sw';
|
||||||
import config from '../conf';
|
import config from '../conf';
|
||||||
|
|
||||||
type ID = string | mongo.ObjectID;
|
type ID = string | mongo.ObjectID;
|
||||||
|
@ -17,6 +18,14 @@ class MisskeyEvent {
|
||||||
this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public publishSw(userId: ID, type: string, value?: any): void {
|
||||||
|
swPush(userId, type, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public publishDriveStream(userId: ID, type: string, value?: any): void {
|
||||||
|
this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
public publishPostStream(postId: ID, type: string, value?: any): void {
|
public publishPostStream(postId: ID, type: string, value?: any): void {
|
||||||
this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
@ -25,6 +34,10 @@ class MisskeyEvent {
|
||||||
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public publishMessagingIndexStream(userId: ID, type: string, value?: any): void {
|
||||||
|
this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
public publishChannelStream(channelId: ID, type: string, value?: any): void {
|
public publishChannelStream(channelId: ID, type: string, value?: any): void {
|
||||||
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
@ -42,8 +55,14 @@ const ev = new MisskeyEvent();
|
||||||
|
|
||||||
export default ev.publishUserStream.bind(ev);
|
export default ev.publishUserStream.bind(ev);
|
||||||
|
|
||||||
|
export const pushSw = ev.publishSw.bind(ev);
|
||||||
|
|
||||||
|
export const publishDriveStream = ev.publishDriveStream.bind(ev);
|
||||||
|
|
||||||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||||
|
|
||||||
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
||||||
|
|
||||||
|
export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
|
||||||
|
|
||||||
export const publishChannelStream = ev.publishChannelStream.bind(ev);
|
export const publishChannelStream = ev.publishChannelStream.bind(ev);
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import db from '../../db/mongodb';
|
import * as mongodb from 'mongodb';
|
||||||
|
import monkDb, { nativeDbConn } from '../../db/mongodb';
|
||||||
|
|
||||||
const collection = db.get('drive_files');
|
const collection = monkDb.get('drive_files.files');
|
||||||
|
|
||||||
(collection as any).createIndex('hash'); // fuck type definition
|
|
||||||
|
|
||||||
export default collection as any; // fuck type definition
|
export default collection as any; // fuck type definition
|
||||||
|
|
||||||
|
const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
|
||||||
|
const db = await nativeDbConn();
|
||||||
|
const bucket = new mongodb.GridFSBucket(db, {
|
||||||
|
bucketName: 'drive_files'
|
||||||
|
});
|
||||||
|
return bucket;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getGridFSBucket };
|
||||||
|
|
||||||
export function validateFileName(name: string): boolean {
|
export function validateFileName(name: string): boolean {
|
||||||
return (
|
return (
|
||||||
(name.trim().length > 0) &&
|
(name.trim().length > 0) &&
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import db from '../../db/mongodb';
|
||||||
|
|
||||||
|
export default db.get('meta') as any; // fuck type definition
|
||||||
|
|
||||||
|
export type IMeta = {
|
||||||
|
top_image: string;
|
||||||
|
};
|
|
@ -1,8 +1,47 @@
|
||||||
import * as mongo from 'mongodb';
|
import * as mongo from 'mongodb';
|
||||||
import db from '../../db/mongodb';
|
import db from '../../db/mongodb';
|
||||||
|
import { IUser } from './user';
|
||||||
|
|
||||||
export default db.get('notifications') as any; // fuck type definition
|
export default db.get('notifications') as any; // fuck type definition
|
||||||
|
|
||||||
export interface INotification {
|
export interface INotification {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知の受信者
|
||||||
|
*/
|
||||||
|
notifiee?: IUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知の受信者
|
||||||
|
*/
|
||||||
|
notifiee_id: mongo.ObjectID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
|
||||||
|
*/
|
||||||
|
notifier?: IUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
|
||||||
|
*/
|
||||||
|
notifier_id: mongo.ObjectID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知の種類。
|
||||||
|
* follow - フォローされた
|
||||||
|
* mention - 投稿で自分が言及された
|
||||||
|
* reply - (自分または自分がWatchしている)投稿が返信された
|
||||||
|
* repost - (自分または自分がWatchしている)投稿がRepostされた
|
||||||
|
* quote - (自分または自分がWatchしている)投稿が引用Repostされた
|
||||||
|
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
||||||
|
* poll_vote - (自分または自分がWatchしている)投稿の投票に投票された
|
||||||
|
*/
|
||||||
|
type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知が読まれたかどうか
|
||||||
|
*/
|
||||||
|
is_read: Boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import db from '../../db/mongodb';
|
||||||
|
|
||||||
|
export default db.get('sw_subscriptions') as any; // fuck type definition
|
|
@ -4,7 +4,7 @@ import { default as User, IUser } from '../models/user';
|
||||||
import Signin from '../models/signin';
|
import Signin from '../models/signin';
|
||||||
import serialize from '../serializers/signin';
|
import serialize from '../serializers/signin';
|
||||||
import event from '../event';
|
import event from '../event';
|
||||||
import config from '../../conf';
|
import signin from '../common/signin';
|
||||||
|
|
||||||
export default async (req: express.Request, res: express.Response) => {
|
export default async (req: express.Request, res: express.Response) => {
|
||||||
res.header('Access-Control-Allow-Credentials', 'true');
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
|
@ -40,20 +40,10 @@ export default async (req: express.Request, res: express.Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = bcrypt.compareSync(password, user.password);
|
const same = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
const expires = 1000 * 60 * 60 * 24 * 365; // One Year
|
signin(res, user, false);
|
||||||
res.cookie('i', user.token, {
|
|
||||||
path: '/',
|
|
||||||
domain: `.${config.host}`,
|
|
||||||
secure: config.url.substr(0, 5) === 'https',
|
|
||||||
httpOnly: false,
|
|
||||||
expires: new Date(Date.now() + expires),
|
|
||||||
maxAge: expires
|
|
||||||
});
|
|
||||||
|
|
||||||
res.sendStatus(204);
|
|
||||||
} else {
|
} else {
|
||||||
res.status(400).send({
|
res.status(400).send({
|
||||||
error: 'incorrect password'
|
error: 'incorrect password'
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as uuid from 'uuid';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from 'bcryptjs';
|
||||||
import recaptcha = require('recaptcha-promise');
|
import recaptcha = require('recaptcha-promise');
|
||||||
|
@ -8,9 +9,31 @@ import generateUserToken from '../common/generate-native-user-token';
|
||||||
import config from '../../conf';
|
import config from '../../conf';
|
||||||
|
|
||||||
recaptcha.init({
|
recaptcha.init({
|
||||||
secret_key: config.recaptcha.secretKey
|
secret_key: config.recaptcha.secret_key
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const home = {
|
||||||
|
left: [
|
||||||
|
'profile',
|
||||||
|
'calendar',
|
||||||
|
'activity',
|
||||||
|
'rss-reader',
|
||||||
|
'trends',
|
||||||
|
'photo-stream',
|
||||||
|
'version'
|
||||||
|
],
|
||||||
|
right: [
|
||||||
|
'broadcast',
|
||||||
|
'notifications',
|
||||||
|
'user-recommendation',
|
||||||
|
'recommended-polls',
|
||||||
|
'server',
|
||||||
|
'donation',
|
||||||
|
'nav',
|
||||||
|
'tips'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
export default async (req: express.Request, res: express.Response) => {
|
export default async (req: express.Request, res: express.Response) => {
|
||||||
// Verify recaptcha
|
// Verify recaptcha
|
||||||
// ただしテスト時はこの機構は障害となるため無効にする
|
// ただしテスト時はこの機構は障害となるため無効にする
|
||||||
|
@ -54,12 +77,34 @@ export default async (req: express.Request, res: express.Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = bcrypt.genSaltSync(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
const hash = bcrypt.hashSync(password, salt);
|
const hash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateUserToken();
|
const secret = generateUserToken();
|
||||||
|
|
||||||
|
//#region Construct home data
|
||||||
|
const homeData = [];
|
||||||
|
|
||||||
|
home.left.forEach(widget => {
|
||||||
|
homeData.push({
|
||||||
|
name: widget,
|
||||||
|
id: uuid(),
|
||||||
|
place: 'left',
|
||||||
|
data: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
home.right.forEach(widget => {
|
||||||
|
homeData.push({
|
||||||
|
name: widget,
|
||||||
|
id: uuid(),
|
||||||
|
place: 'right',
|
||||||
|
data: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// Create account
|
// Create account
|
||||||
const account: IUser = await User.insert({
|
const account: IUser = await User.insert({
|
||||||
token: secret,
|
token: secret,
|
||||||
|
@ -88,6 +133,11 @@ export default async (req: express.Request, res: express.Response) => {
|
||||||
height: null,
|
height: null,
|
||||||
location: null,
|
location: null,
|
||||||
weight: null
|
weight: null
|
||||||
|
},
|
||||||
|
settings: {},
|
||||||
|
client_settings: {
|
||||||
|
home: homeData,
|
||||||
|
show_donation: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -31,44 +31,44 @@ export default (
|
||||||
if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
|
if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
|
||||||
_file = await DriveFile.findOne({
|
_file = await DriveFile.findOne({
|
||||||
_id: file
|
_id: file
|
||||||
}, {
|
});
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (typeof file === 'string') {
|
} else if (typeof file === 'string') {
|
||||||
_file = await DriveFile.findOne({
|
_file = await DriveFile.findOne({
|
||||||
_id: new mongo.ObjectID(file)
|
_id: new mongo.ObjectID(file)
|
||||||
}, {
|
});
|
||||||
fields: {
|
|
||||||
data: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_file = deepcopy(file);
|
_file = deepcopy(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename _id to id
|
if (!_file) return reject('invalid file arg.');
|
||||||
_file.id = _file._id;
|
|
||||||
delete _file._id;
|
|
||||||
|
|
||||||
delete _file.data;
|
// rendered target
|
||||||
|
let _target: any = {};
|
||||||
|
|
||||||
_file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`;
|
_target.id = _file._id;
|
||||||
|
_target.created_at = _file.uploadDate;
|
||||||
|
_target.name = _file.filename;
|
||||||
|
_target.type = _file.contentType;
|
||||||
|
_target.datasize = _file.length;
|
||||||
|
_target.md5 = _file.md5;
|
||||||
|
|
||||||
if (opts.detail && _file.folder_id) {
|
_target = Object.assign(_target, _file.metadata);
|
||||||
|
|
||||||
|
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
|
||||||
|
|
||||||
|
if (opts.detail && _target.folder_id) {
|
||||||
// Populate folder
|
// Populate folder
|
||||||
_file.folder = await serializeDriveFolder(_file.folder_id, {
|
_target.folder = await serializeDriveFolder(_target.folder_id, {
|
||||||
detail: true
|
detail: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.detail && _file.tags) {
|
if (opts.detail && _target.tags) {
|
||||||
// Populate tags
|
// Populate tags
|
||||||
_file.tags = await _file.tags.map(async (tag: any) =>
|
_target.tags = await _target.tags.map(async (tag: any) =>
|
||||||
await serializeDriveTag(tag)
|
await serializeDriveTag(tag)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(_file);
|
resolve(_target);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,7 +44,7 @@ const self = (
|
||||||
});
|
});
|
||||||
|
|
||||||
const childFilesCount = await DriveFile.count({
|
const childFilesCount = await DriveFile.count({
|
||||||
folder_id: _folder.id
|
'metadata.folder_id': _folder.id
|
||||||
});
|
});
|
||||||
|
|
||||||
_folder.folders_count = childFoldersCount;
|
_folder.folders_count = childFoldersCount;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import serializeChannel from './channel';
|
||||||
import serializeUser from './user';
|
import serializeUser from './user';
|
||||||
import serializeDriveFile from './drive-file';
|
import serializeDriveFile from './drive-file';
|
||||||
import parse from '../common/text';
|
import parse from '../common/text';
|
||||||
|
import rap from '@prezzemolo/rap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a post
|
* Serialize a post
|
||||||
|
@ -21,13 +22,13 @@ import parse from '../common/text';
|
||||||
* @param options? serialize options
|
* @param options? serialize options
|
||||||
* @return response
|
* @return response
|
||||||
*/
|
*/
|
||||||
const self = (
|
const self = async (
|
||||||
post: string | mongo.ObjectID | IPost,
|
post: string | mongo.ObjectID | IPost,
|
||||||
me?: string | mongo.ObjectID | IUser,
|
me?: string | mongo.ObjectID | IUser,
|
||||||
options?: {
|
options?: {
|
||||||
detail: boolean
|
detail: boolean
|
||||||
}
|
}
|
||||||
) => new Promise<any>(async (resolve, reject) => {
|
) => {
|
||||||
const opts = options || {
|
const opts = options || {
|
||||||
detail: true,
|
detail: true,
|
||||||
};
|
};
|
||||||
|
@ -56,6 +57,8 @@ const self = (
|
||||||
_post = deepcopy(post);
|
_post = deepcopy(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_post) throw 'invalid post arg.';
|
||||||
|
|
||||||
const id = _post._id;
|
const id = _post._id;
|
||||||
|
|
||||||
// Rename _id to id
|
// Rename _id to id
|
||||||
|
@ -70,105 +73,120 @@ const self = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate user
|
// Populate user
|
||||||
_post.user = await serializeUser(_post.user_id, meId);
|
_post.user = serializeUser(_post.user_id, meId);
|
||||||
|
|
||||||
// Populate app
|
// Populate app
|
||||||
if (_post.app_id) {
|
if (_post.app_id) {
|
||||||
_post.app = await serializeApp(_post.app_id);
|
_post.app = serializeApp(_post.app_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate channel
|
// Populate channel
|
||||||
if (_post.channel_id) {
|
if (_post.channel_id) {
|
||||||
_post.channel = await serializeChannel(_post.channel_id);
|
_post.channel = serializeChannel(_post.channel_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate media
|
// Populate media
|
||||||
if (_post.media_ids) {
|
if (_post.media_ids) {
|
||||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
_post.media = Promise.all(_post.media_ids.map(fileId =>
|
||||||
await serializeDriveFile(fileId)
|
serializeDriveFile(fileId)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// When requested a detailed post data
|
// When requested a detailed post data
|
||||||
if (opts.detail) {
|
if (opts.detail) {
|
||||||
// Get previous post info
|
// Get previous post info
|
||||||
const prev = await Post.findOne({
|
_post.prev = (async () => {
|
||||||
user_id: _post.user_id,
|
const prev = await Post.findOne({
|
||||||
_id: {
|
user_id: _post.user_id,
|
||||||
$lt: id
|
_id: {
|
||||||
}
|
$lt: id
|
||||||
}, {
|
}
|
||||||
fields: {
|
}, {
|
||||||
_id: true
|
fields: {
|
||||||
},
|
_id: true
|
||||||
sort: {
|
},
|
||||||
_id: -1
|
sort: {
|
||||||
}
|
_id: -1
|
||||||
});
|
}
|
||||||
_post.prev = prev ? prev._id : null;
|
});
|
||||||
|
return prev ? prev._id : null;
|
||||||
|
})();
|
||||||
|
|
||||||
// Get next post info
|
// Get next post info
|
||||||
const next = await Post.findOne({
|
_post.next = (async () => {
|
||||||
user_id: _post.user_id,
|
const next = await Post.findOne({
|
||||||
_id: {
|
user_id: _post.user_id,
|
||||||
$gt: id
|
_id: {
|
||||||
}
|
$gt: id
|
||||||
}, {
|
}
|
||||||
fields: {
|
}, {
|
||||||
_id: true
|
fields: {
|
||||||
},
|
_id: true
|
||||||
sort: {
|
},
|
||||||
_id: 1
|
sort: {
|
||||||
}
|
_id: 1
|
||||||
});
|
}
|
||||||
_post.next = next ? next._id : null;
|
});
|
||||||
|
return next ? next._id : null;
|
||||||
|
})();
|
||||||
|
|
||||||
if (_post.reply_id) {
|
if (_post.reply_id) {
|
||||||
// Populate reply to post
|
// Populate reply to post
|
||||||
_post.reply = await self(_post.reply_id, meId, {
|
_post.reply = self(_post.reply_id, meId, {
|
||||||
detail: false
|
detail: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_post.repost_id) {
|
if (_post.repost_id) {
|
||||||
// Populate repost
|
// Populate repost
|
||||||
_post.repost = await self(_post.repost_id, meId, {
|
_post.repost = self(_post.repost_id, meId, {
|
||||||
detail: _post.text == null
|
detail: _post.text == null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll
|
// Poll
|
||||||
if (meId && _post.poll) {
|
if (meId && _post.poll) {
|
||||||
const vote = await Vote
|
_post.poll = (async (poll) => {
|
||||||
.findOne({
|
const vote = await Vote
|
||||||
user_id: meId,
|
.findOne({
|
||||||
post_id: id
|
user_id: meId,
|
||||||
});
|
post_id: id
|
||||||
|
});
|
||||||
|
|
||||||
if (vote != null) {
|
if (vote != null) {
|
||||||
const myChoice = _post.poll.choices
|
const myChoice = poll.choices
|
||||||
.filter(c => c.id == vote.choice)[0];
|
.filter(c => c.id == vote.choice)[0];
|
||||||
|
|
||||||
myChoice.is_voted = true;
|
myChoice.is_voted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return poll;
|
||||||
|
})(_post.poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch my reaction
|
// Fetch my reaction
|
||||||
if (meId) {
|
if (meId) {
|
||||||
const reaction = await Reaction
|
_post.my_reaction = (async () => {
|
||||||
.findOne({
|
const reaction = await Reaction
|
||||||
user_id: meId,
|
.findOne({
|
||||||
post_id: id,
|
user_id: meId,
|
||||||
deleted_at: { $exists: false }
|
post_id: id,
|
||||||
});
|
deleted_at: { $exists: false }
|
||||||
|
});
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
_post.my_reaction = reaction.reaction;
|
return reaction.reaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(_post);
|
// resolve promises in _post object
|
||||||
});
|
_post = await rap(_post);
|
||||||
|
|
||||||
|
return _post;
|
||||||
|
};
|
||||||
|
|
||||||
export default self;
|
export default self;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import serializePost from './post';
|
||||||
import Following from '../models/following';
|
import Following from '../models/following';
|
||||||
import getFriends from '../common/get-friends';
|
import getFriends from '../common/get-friends';
|
||||||
import config from '../../conf';
|
import config from '../../conf';
|
||||||
|
import rap from '@prezzemolo/rap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a user
|
* Serialize a user
|
||||||
|
@ -34,9 +35,10 @@ export default (
|
||||||
let _user: any;
|
let _user: any;
|
||||||
|
|
||||||
const fields = opts.detail ? {
|
const fields = opts.detail ? {
|
||||||
data: false
|
settings: false
|
||||||
} : {
|
} : {
|
||||||
data: false,
|
settings: false,
|
||||||
|
client_settings: false,
|
||||||
profile: false,
|
profile: false,
|
||||||
keywords: false,
|
keywords: false,
|
||||||
domains: false
|
domains: false
|
||||||
|
@ -55,6 +57,8 @@ export default (
|
||||||
_user = deepcopy(user);
|
_user = deepcopy(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_user) return reject('invalid user arg.');
|
||||||
|
|
||||||
// Me
|
// Me
|
||||||
const meId: mongo.ObjectID = me
|
const meId: mongo.ObjectID = me
|
||||||
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
||||||
|
@ -69,7 +73,7 @@ export default (
|
||||||
delete _user._id;
|
delete _user._id;
|
||||||
|
|
||||||
// Remove needless properties
|
// Remove needless properties
|
||||||
delete _user.lates_post;
|
delete _user.latest_post;
|
||||||
|
|
||||||
// Remove private properties
|
// Remove private properties
|
||||||
delete _user.password;
|
delete _user.password;
|
||||||
|
@ -83,8 +87,8 @@ export default (
|
||||||
|
|
||||||
// Visible via only the official client
|
// Visible via only the official client
|
||||||
if (!opts.includeSecrets) {
|
if (!opts.includeSecrets) {
|
||||||
delete _user.data;
|
|
||||||
delete _user.email;
|
delete _user.email;
|
||||||
|
delete _user.client_settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
_user.avatar_url = _user.avatar_id != null
|
_user.avatar_url = _user.avatar_id != null
|
||||||
|
@ -104,26 +108,30 @@ export default (
|
||||||
|
|
||||||
if (meId && !meId.equals(_user.id)) {
|
if (meId && !meId.equals(_user.id)) {
|
||||||
// If the user is following
|
// If the user is following
|
||||||
const follow = await Following.findOne({
|
_user.is_following = (async () => {
|
||||||
follower_id: meId,
|
const follow = await Following.findOne({
|
||||||
followee_id: _user.id,
|
follower_id: meId,
|
||||||
deleted_at: { $exists: false }
|
followee_id: _user.id,
|
||||||
});
|
deleted_at: { $exists: false }
|
||||||
_user.is_following = follow !== null;
|
});
|
||||||
|
return follow !== null;
|
||||||
|
})();
|
||||||
|
|
||||||
// If the user is followed
|
// If the user is followed
|
||||||
const follow2 = await Following.findOne({
|
_user.is_followed = (async () => {
|
||||||
follower_id: _user.id,
|
const follow2 = await Following.findOne({
|
||||||
followee_id: meId,
|
follower_id: _user.id,
|
||||||
deleted_at: { $exists: false }
|
followee_id: meId,
|
||||||
});
|
deleted_at: { $exists: false }
|
||||||
_user.is_followed = follow2 !== null;
|
});
|
||||||
|
return follow2 !== null;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.detail) {
|
if (opts.detail) {
|
||||||
if (_user.pinned_post_id) {
|
if (_user.pinned_post_id) {
|
||||||
// Populate pinned post
|
// Populate pinned post
|
||||||
_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
|
_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
|
||||||
detail: true
|
detail: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -132,23 +140,24 @@ export default (
|
||||||
const myFollowingIds = await getFriends(meId);
|
const myFollowingIds = await getFriends(meId);
|
||||||
|
|
||||||
// Get following you know count
|
// Get following you know count
|
||||||
const followingYouKnowCount = await Following.count({
|
_user.following_you_know_count = Following.count({
|
||||||
followee_id: { $in: myFollowingIds },
|
followee_id: { $in: myFollowingIds },
|
||||||
follower_id: _user.id,
|
follower_id: _user.id,
|
||||||
deleted_at: { $exists: false }
|
deleted_at: { $exists: false }
|
||||||
});
|
});
|
||||||
_user.following_you_know_count = followingYouKnowCount;
|
|
||||||
|
|
||||||
// Get followers you know count
|
// Get followers you know count
|
||||||
const followersYouKnowCount = await Following.count({
|
_user.followers_you_know_count = Following.count({
|
||||||
followee_id: _user.id,
|
followee_id: _user.id,
|
||||||
follower_id: { $in: myFollowingIds },
|
follower_id: { $in: myFollowingIds },
|
||||||
deleted_at: { $exists: false }
|
deleted_at: { $exists: false }
|
||||||
});
|
});
|
||||||
_user.followers_you_know_count = followersYouKnowCount;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolve promises in _user object
|
||||||
|
_user = await rap(_user);
|
||||||
|
|
||||||
resolve(_user);
|
resolve(_user);
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -40,7 +40,7 @@ app.get('/', (req, res) => {
|
||||||
endpoints.forEach(endpoint =>
|
endpoints.forEach(endpoint =>
|
||||||
endpoint.withFile ?
|
endpoint.withFile ?
|
||||||
app.post(`/${endpoint.name}`,
|
app.post(`/${endpoint.name}`,
|
||||||
endpoint.withFile ? multer({ dest: 'uploads/' }).single('file') : null,
|
endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null,
|
||||||
require('./api-handler').default.bind(null, endpoint)) :
|
require('./api-handler').default.bind(null, endpoint)) :
|
||||||
app.post(`/${endpoint.name}`,
|
app.post(`/${endpoint.name}`,
|
||||||
require('./api-handler').default.bind(null, endpoint))
|
require('./api-handler').default.bind(null, endpoint))
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import * as cookie from 'cookie';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
// import * as Twitter from 'twitter';
|
// import * as Twitter from 'twitter';
|
||||||
// const Twitter = require('twitter');
|
// const Twitter = require('twitter');
|
||||||
import autwh from 'autwh';
|
import autwh from 'autwh';
|
||||||
|
@ -7,6 +9,7 @@ import User from '../models/user';
|
||||||
import serialize from '../serializers/user';
|
import serialize from '../serializers/user';
|
||||||
import event from '../event';
|
import event from '../event';
|
||||||
import config from '../../conf';
|
import config from '../../conf';
|
||||||
|
import signin from '../common/signin';
|
||||||
|
|
||||||
module.exports = (app: express.Application) => {
|
module.exports = (app: express.Application) => {
|
||||||
app.get('/disconnect/twitter', async (req, res): Promise<any> => {
|
app.get('/disconnect/twitter', async (req, res): Promise<any> => {
|
||||||
|
@ -30,8 +33,13 @@ module.exports = (app: express.Application) => {
|
||||||
|
|
||||||
if (config.twitter == null) {
|
if (config.twitter == null) {
|
||||||
app.get('/connect/twitter', (req, res) => {
|
app.get('/connect/twitter', (req, res) => {
|
||||||
res.send('現在Twitterへ接続できません');
|
res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/signin/twitter', (req, res) => {
|
||||||
|
res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,14 +56,58 @@ module.exports = (app: express.Application) => {
|
||||||
res.redirect(ctx.url);
|
res.redirect(ctx.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/tw/cb', (req, res): any => {
|
app.get('/signin/twitter', async (req, res): Promise<any> => {
|
||||||
if (res.locals.user == null) return res.send('plz signin');
|
const ctx = await twAuth.begin();
|
||||||
redis.get(res.locals.user, async (_, ctx) => {
|
|
||||||
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
|
|
||||||
|
|
||||||
const user = await User.findOneAndUpdate({
|
const sessid = uuid();
|
||||||
token: res.locals.user
|
|
||||||
}, {
|
redis.set(sessid, JSON.stringify(ctx));
|
||||||
|
|
||||||
|
const expires = 1000 * 60 * 60; // 1h
|
||||||
|
res.cookie('signin_with_twitter_session_id', sessid, {
|
||||||
|
path: '/',
|
||||||
|
domain: `.${config.host}`,
|
||||||
|
secure: config.url.substr(0, 5) === 'https',
|
||||||
|
httpOnly: true,
|
||||||
|
expires: new Date(Date.now() + expires),
|
||||||
|
maxAge: expires
|
||||||
|
});
|
||||||
|
|
||||||
|
res.redirect(ctx.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/tw/cb', (req, res): any => {
|
||||||
|
if (res.locals.user == null) {
|
||||||
|
// req.headers['cookie'] は常に string ですが、型定義の都合上
|
||||||
|
// string | string[] になっているので string を明示しています
|
||||||
|
const cookies = cookie.parse((req.headers['cookie'] as string || ''));
|
||||||
|
|
||||||
|
const sessid = cookies['signin_with_twitter_session_id'];
|
||||||
|
|
||||||
|
if (sessid == undefined) {
|
||||||
|
res.status(400).send('invalid session');
|
||||||
|
}
|
||||||
|
|
||||||
|
redis.get(sessid, async (_, ctx) => {
|
||||||
|
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
'twitter.user_id': result.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
signin(res, user, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
redis.get(res.locals.user, async (_, ctx) => {
|
||||||
|
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
|
||||||
|
|
||||||
|
const user = await User.findOneAndUpdate({
|
||||||
|
token: res.locals.user
|
||||||
|
}, {
|
||||||
$set: {
|
$set: {
|
||||||
twitter: {
|
twitter: {
|
||||||
access_token: result.accessToken,
|
access_token: result.accessToken,
|
||||||
|
@ -66,13 +118,14 @@ module.exports = (app: express.Application) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
|
res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
|
||||||
|
|
||||||
// Publish i updated event
|
// Publish i updated event
|
||||||
event(user._id, 'i_updated', await serialize(user, user, {
|
event(user._id, 'i_updated', await serialize(user, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
includeSecrets: true
|
includeSecrets: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as websocket from 'websocket';
|
||||||
|
import * as redis from 'redis';
|
||||||
|
|
||||||
|
export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
|
||||||
|
// Subscribe drive stream
|
||||||
|
subscriber.subscribe(`misskey:drive-stream:${user._id}`);
|
||||||
|
subscriber.on('message', (_, data) => {
|
||||||
|
connection.send(data);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as websocket from 'websocket';
|
||||||
|
import * as redis from 'redis';
|
||||||
|
|
||||||
|
export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
|
||||||
|
// Subscribe messaging index stream
|
||||||
|
subscriber.subscribe(`misskey:messaging-index-stream:${user._id}`);
|
||||||
|
subscriber.on('message', (_, data) => {
|
||||||
|
connection.send(data);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as websocket from 'websocket';
|
||||||
|
import Xev from 'xev';
|
||||||
|
|
||||||
|
const ev = new Xev();
|
||||||
|
|
||||||
|
export default function homeStream(request: websocket.request, connection: websocket.connection): void {
|
||||||
|
const onRequest = request => {
|
||||||
|
connection.send(JSON.stringify({
|
||||||
|
type: 'request',
|
||||||
|
body: request
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ev.addListener('request', onRequest);
|
||||||
|
|
||||||
|
connection.on('close', () => {
|
||||||
|
ev.removeListener('request', onRequest);
|
||||||
|
});
|
||||||
|
}
|
|
@ -7,8 +7,11 @@ import AccessToken from './models/access-token';
|
||||||
import isNativeToken from './common/is-native-token';
|
import isNativeToken from './common/is-native-token';
|
||||||
|
|
||||||
import homeStream from './stream/home';
|
import homeStream from './stream/home';
|
||||||
|
import driveStream from './stream/drive';
|
||||||
import messagingStream from './stream/messaging';
|
import messagingStream from './stream/messaging';
|
||||||
|
import messagingIndexStream from './stream/messaging-index';
|
||||||
import serverStream from './stream/server';
|
import serverStream from './stream/server';
|
||||||
|
import requestsStream from './stream/requests';
|
||||||
import channelStream from './stream/channel';
|
import channelStream from './stream/channel';
|
||||||
|
|
||||||
module.exports = (server: http.Server) => {
|
module.exports = (server: http.Server) => {
|
||||||
|
@ -27,6 +30,11 @@ module.exports = (server: http.Server) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.resourceURL.pathname === '/requests') {
|
||||||
|
requestsStream(request, connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to Redis
|
// Connect to Redis
|
||||||
const subscriber = redis.createClient(
|
const subscriber = redis.createClient(
|
||||||
config.redis.port, config.redis.host);
|
config.redis.port, config.redis.host);
|
||||||
|
@ -51,7 +59,9 @@ module.exports = (server: http.Server) => {
|
||||||
|
|
||||||
const channel =
|
const channel =
|
||||||
request.resourceURL.pathname === '/' ? homeStream :
|
request.resourceURL.pathname === '/' ? homeStream :
|
||||||
|
request.resourceURL.pathname === '/drive' ? driveStream :
|
||||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||||
|
request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
if (channel !== null) {
|
if (channel !== null) {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import getPostSummary from './get-post-summary';
|
||||||
|
import getReactionEmoji from './get-reaction-emoji';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知を表す文字列を取得します。
|
||||||
|
* @param notification 通知
|
||||||
|
*/
|
||||||
|
export default function(notification: any): string {
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'follow':
|
||||||
|
return `${notification.user.name}にフォローされました`;
|
||||||
|
case 'mention':
|
||||||
|
return `言及されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
|
||||||
|
case 'reply':
|
||||||
|
return `返信されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
|
||||||
|
case 'repost':
|
||||||
|
return `Repostされました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
|
||||||
|
case 'quote':
|
||||||
|
return `引用されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
|
||||||
|
case 'reaction':
|
||||||
|
return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
|
||||||
|
case 'poll_vote':
|
||||||
|
return `投票されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
|
||||||
|
default:
|
||||||
|
return `<不明な通知タイプ: ${notification.type}>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default function(reaction: string): string {
|
||||||
|
switch (reaction) {
|
||||||
|
case 'like': return '👍';
|
||||||
|
case 'love': return '❤️';
|
||||||
|
case 'laugh': return '😆';
|
||||||
|
case 'hmm': return '🤔';
|
||||||
|
case 'surprise': return '😮';
|
||||||
|
case 'congrats': return '🎉';
|
||||||
|
case 'angry': return '💢';
|
||||||
|
case 'confused': return '😥';
|
||||||
|
case 'pudding': return '🍮';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as URL from 'url';
|
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import isUrl = require('is-url');
|
import isUrl = require('is-url');
|
||||||
|
|
||||||
|
@ -23,16 +22,23 @@ export const path = process.env.NODE_ENV == 'test'
|
||||||
* ユーザーが設定する必要のある情報
|
* ユーザーが設定する必要のある情報
|
||||||
*/
|
*/
|
||||||
type Source = {
|
type Source = {
|
||||||
maintainer: string;
|
/**
|
||||||
|
* メンテナ情報
|
||||||
|
*/
|
||||||
|
maintainer: {
|
||||||
|
/**
|
||||||
|
* メンテナの名前
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* メンテナの連絡先(URLかmailto形式のURL)
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
url: string;
|
url: string;
|
||||||
secondary_url: string;
|
secondary_url: string;
|
||||||
port: number;
|
port: number;
|
||||||
https: {
|
https?: { [x: string]: string };
|
||||||
enable: boolean;
|
|
||||||
key: string;
|
|
||||||
cert: string;
|
|
||||||
ca: string;
|
|
||||||
};
|
|
||||||
mongodb: {
|
mongodb: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
@ -52,8 +58,8 @@ type Source = {
|
||||||
pass: string;
|
pass: string;
|
||||||
};
|
};
|
||||||
recaptcha: {
|
recaptcha: {
|
||||||
siteKey: string;
|
site_key: string;
|
||||||
secretKey: string;
|
secret_key: string;
|
||||||
};
|
};
|
||||||
accesslog?: string;
|
accesslog?: string;
|
||||||
accesses?: {
|
accesses?: {
|
||||||
|
@ -75,6 +81,14 @@ type Source = {
|
||||||
analysis?: {
|
analysis?: {
|
||||||
mecab_command?: string;
|
mecab_command?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Worker
|
||||||
|
*/
|
||||||
|
sw?: {
|
||||||
|
public_key: string;
|
||||||
|
private_key: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,14 +120,6 @@ export default function load() {
|
||||||
if (!isUrl(config.url)) urlError(config.url);
|
if (!isUrl(config.url)) urlError(config.url);
|
||||||
if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
|
if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
|
||||||
|
|
||||||
const url = URL.parse(config.url);
|
|
||||||
const head = url.host.split('.')[0];
|
|
||||||
|
|
||||||
if (head != 'misskey') {
|
|
||||||
console.error(`プライマリドメインは、必ず「misskey」ドメインで始まっていなければなりません(現在の設定では「${head}」で始まっています)。例えば「https://misskey.xyz」「http://misskey.my.app.example.com」などが正しいプライマリURLです。`);
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
config.url = normalizeUrl(config.url);
|
config.url = normalizeUrl(config.url);
|
||||||
config.secondary_url = normalizeUrl(config.secondary_url);
|
config.secondary_url = normalizeUrl(config.secondary_url);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"themeColor": "#f43636",
|
"themeColor": "#ff4e45",
|
||||||
"themeColorForeground": "#fff"
|
"themeColorForeground": "#fff"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,38 @@
|
||||||
import * as mongo from 'monk';
|
|
||||||
|
|
||||||
import config from '../conf';
|
import config from '../conf';
|
||||||
|
|
||||||
const uri = config.mongodb.user && config.mongodb.pass
|
const uri = config.mongodb.user && config.mongodb.pass
|
||||||
? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
|
? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
|
||||||
: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* monk
|
||||||
|
*/
|
||||||
|
import * as mongo from 'monk';
|
||||||
|
|
||||||
const db = mongo(uri);
|
const db = mongo(uri);
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MongoDB native module (officialy)
|
||||||
|
*/
|
||||||
|
import * as mongodb from 'mongodb';
|
||||||
|
|
||||||
|
let mdb: mongodb.Db;
|
||||||
|
|
||||||
|
const nativeDbConn = async (): Promise<mongodb.Db> => {
|
||||||
|
if (mdb) return mdb;
|
||||||
|
|
||||||
|
const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
|
||||||
|
mongodb.MongoClient.connect(uri, (e, db) => {
|
||||||
|
if (e) return reject(e);
|
||||||
|
resolve(db);
|
||||||
|
});
|
||||||
|
}))();
|
||||||
|
|
||||||
|
mdb = db;
|
||||||
|
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { nativeDbConn };
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
|
@ -8,8 +8,9 @@ import * as bodyParser from 'body-parser';
|
||||||
import * as cors from 'cors';
|
import * as cors from 'cors';
|
||||||
import * as mongodb from 'mongodb';
|
import * as mongodb from 'mongodb';
|
||||||
import * as gm from 'gm';
|
import * as gm from 'gm';
|
||||||
|
import * as stream from 'stream';
|
||||||
|
|
||||||
import File from '../api/models/drive-file';
|
import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init app
|
* Init app
|
||||||
|
@ -33,101 +34,127 @@ app.get('/', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/default-avatar.jpg', (req, res) => {
|
app.get('/default-avatar.jpg', (req, res) => {
|
||||||
const file = fs.readFileSync(`${__dirname}/assets/avatar.jpg`);
|
const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`);
|
||||||
send(file, 'image/jpeg', req, res);
|
send(file, 'image/jpeg', req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/app-default.jpg', (req, res) => {
|
app.get('/app-default.jpg', (req, res) => {
|
||||||
const file = fs.readFileSync(`${__dirname}/assets/dummy.png`);
|
const file = fs.createReadStream(`${__dirname}/assets/dummy.png`);
|
||||||
send(file, 'image/png', req, res);
|
send(file, 'image/png', req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function raw(data: Buffer, type: string, download: boolean, res: express.Response): Promise<any> {
|
interface ISend {
|
||||||
res.header('Content-Type', type);
|
contentType: string;
|
||||||
|
stream: stream.Readable;
|
||||||
if (download) {
|
|
||||||
res.header('Content-Disposition', 'attachment');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function thumbnail(data: Buffer, type: string, resize: number, res: express.Response): Promise<any> {
|
function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
|
||||||
if (!/^image\/.*$/.test(type)) {
|
const readable: stream.Readable = (() => {
|
||||||
data = fs.readFileSync(`${__dirname}/assets/dummy.png`);
|
// 画像ではない場合
|
||||||
}
|
if (!/^image\/.*$/.test(type)) {
|
||||||
|
// 使わないことにしたストリームはしっかり取り壊しておく
|
||||||
|
data.destroy();
|
||||||
|
return fs.createReadStream(`${__dirname}/assets/not-an-image.png`);
|
||||||
|
}
|
||||||
|
|
||||||
let g = gm(data);
|
const imageType = type.split('/')[1];
|
||||||
|
|
||||||
|
// 画像でもPNGかJPEGでないならダメ
|
||||||
|
if (imageType != 'png' && imageType != 'jpeg') {
|
||||||
|
// 使わないことにしたストリームはしっかり取り壊しておく
|
||||||
|
data.destroy();
|
||||||
|
return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})();
|
||||||
|
|
||||||
|
let g = gm(readable);
|
||||||
|
|
||||||
if (resize) {
|
if (resize) {
|
||||||
g = g.resize(resize, resize);
|
g = g.resize(resize, resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
g
|
const stream = g
|
||||||
.compress('jpeg')
|
.compress('jpeg')
|
||||||
.quality(80)
|
.quality(80)
|
||||||
.toBuffer('jpeg', (err, img) => {
|
.stream();
|
||||||
if (err !== undefined && err !== null) {
|
|
||||||
console.error(err);
|
|
||||||
res.sendStatus(500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header('Content-Type', 'image/jpeg');
|
return {
|
||||||
res.send(img);
|
contentType: 'image/jpeg',
|
||||||
});
|
stream
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(data: Buffer, type: string, req: express.Request, res: express.Response): void {
|
const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => {
|
||||||
if (req.query.thumbnail !== undefined) {
|
console.dir(e);
|
||||||
thumbnail(data, type, req.query.size, res);
|
req.destroy();
|
||||||
} else {
|
res.destroy(e);
|
||||||
raw(data, type, req.query.download !== undefined, res);
|
};
|
||||||
|
|
||||||
|
function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void {
|
||||||
|
readable.on('error', commonReadableHandlerGenerator(req, res));
|
||||||
|
|
||||||
|
const data = ((): ISend => {
|
||||||
|
if (req.query.thumbnail !== undefined) {
|
||||||
|
return thumbnail(readable, type, req.query.size);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contentType: type,
|
||||||
|
stream: readable
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (readable !== data.stream) {
|
||||||
|
data.stream.on('error', commonReadableHandlerGenerator(req, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.query.download !== undefined) {
|
||||||
|
res.header('Content-Disposition', 'attachment');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.header('Content-Type', data.contentType);
|
||||||
|
|
||||||
|
data.stream.pipe(res);
|
||||||
|
|
||||||
|
data.stream.on('end', () => {
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendFileById(req: express.Request, res: express.Response): Promise<void> {
|
||||||
|
// Validate id
|
||||||
|
if (!mongodb.ObjectID.isValid(req.params.id)) {
|
||||||
|
res.status(400).send('incorrect id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = new mongodb.ObjectID(req.params.id);
|
||||||
|
const file = await DriveFile.findOne({ _id: fileId });
|
||||||
|
|
||||||
|
// validate name
|
||||||
|
if (req.params.name !== undefined && req.params.name !== file.filename) {
|
||||||
|
res.status(404).send('there is no file has given name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = await getGridFSBucket();
|
||||||
|
|
||||||
|
const readable = bucket.openDownloadStream(fileId);
|
||||||
|
|
||||||
|
send(readable, file.contentType, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing
|
* Routing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.get('/:id', async (req, res) => {
|
app.get('/:id', sendFileById);
|
||||||
// Validate id
|
app.get('/:id/:name', sendFileById);
|
||||||
if (!mongodb.ObjectID.isValid(req.params.id)) {
|
|
||||||
res.status(400).send('incorrect id');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
|
|
||||||
|
|
||||||
if (file == null) {
|
|
||||||
res.status(404).sendFile(`${__dirname} / assets / dummy.png`);
|
|
||||||
return;
|
|
||||||
} else if (file.data == null) {
|
|
||||||
res.sendStatus(400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
send(file.data.buffer, file.type, req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/:id/:name', async (req, res) => {
|
|
||||||
// Validate id
|
|
||||||
if (!mongodb.ObjectID.isValid(req.params.id)) {
|
|
||||||
res.status(400).send('incorrect id');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
|
|
||||||
|
|
||||||
if (file == null) {
|
|
||||||
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
|
|
||||||
return;
|
|
||||||
} else if (file.data == null) {
|
|
||||||
res.sendStatus(400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
send(file.data.buffer, file.type, req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as cluster from 'cluster';
|
import * as cluster from 'cluster';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import * as chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
// import portUsed = require('tcp-port-used');
|
// import portUsed = require('tcp-port-used');
|
||||||
import isRoot = require('is-root');
|
import isRoot = require('is-root');
|
||||||
import { master } from 'accesses';
|
import { master } from 'accesses';
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as express from 'express';
|
||||||
|
import * as proxyAddr from 'proxy-addr';
|
||||||
|
import Xev from 'xev';
|
||||||
|
|
||||||
|
const ev = new Xev();
|
||||||
|
|
||||||
|
export default function(req: express.Request) {
|
||||||
|
const ip = proxyAddr(req, () => true);
|
||||||
|
|
||||||
|
const md5 = crypto.createHash('md5');
|
||||||
|
md5.update(ip);
|
||||||
|
const hashedIp = md5.digest('hex').substr(0, 3);
|
||||||
|
|
||||||
|
ev.emit('request', {
|
||||||
|
ip: hashedIp,
|
||||||
|
method: req.method,
|
||||||
|
hostname: req.hostname,
|
||||||
|
path: req.originalUrl
|
||||||
|
});
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import * as morgan from 'morgan';
|
||||||
import Accesses from 'accesses';
|
import Accesses from 'accesses';
|
||||||
import vhost = require('vhost');
|
import vhost = require('vhost');
|
||||||
|
|
||||||
|
import log from './log-request';
|
||||||
import config from './conf';
|
import config from './conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,7 +36,12 @@ app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', {
|
||||||
stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null
|
stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Drop request that without 'Host' header
|
app.use((req, res, next) => {
|
||||||
|
log(req);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drop request when without 'Host' header
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (!req.headers['host']) {
|
if (!req.headers['host']) {
|
||||||
res.sendStatus(400);
|
res.sendStatus(400);
|
||||||
|
@ -55,13 +61,17 @@ app.use(require('./web/server'));
|
||||||
/**
|
/**
|
||||||
* Create server
|
* Create server
|
||||||
*/
|
*/
|
||||||
const server = config.https.enable ?
|
const server = (() => {
|
||||||
https.createServer({
|
if (config.https) {
|
||||||
key: fs.readFileSync(config.https.key),
|
const certs = {};
|
||||||
cert: fs.readFileSync(config.https.cert),
|
Object.keys(config.https).forEach(k => {
|
||||||
ca: fs.readFileSync(config.https.ca)
|
certs[k] = fs.readFileSync(config.https[k]);
|
||||||
}, app) :
|
});
|
||||||
http.createServer(app);
|
return https.createServer(certs, app);
|
||||||
|
} else {
|
||||||
|
return http.createServer(app);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steaming
|
* Steaming
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress bar
|
* Progress bar
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as chalk from 'chalk';
|
import chalk, { Chalk } from 'chalk';
|
||||||
|
|
||||||
export type LogLevel = 'Error' | 'Warn' | 'Info';
|
export type LogLevel = 'Error' | 'Warn' | 'Info';
|
||||||
|
|
||||||
function toLevelColor(level: LogLevel): chalk.ChalkStyle {
|
function toLevelColor(level: LogLevel): Chalk {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'Error': return chalk.red;
|
case 'Error': return chalk.red;
|
||||||
case 'Warn': return chalk.yellow;
|
case 'Warn': return chalk.yellow;
|
||||||
|
|
|
@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
|
||||||
/**
|
/**
|
||||||
* init
|
* init
|
||||||
*/
|
*/
|
||||||
init(me => {
|
init(() => {
|
||||||
mount(document.createElement('mk-index'));
|
mount(document.createElement('mk-index'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ html
|
||||||
meta(name='application-name' content='Misskey')
|
meta(name='application-name' content='Misskey')
|
||||||
meta(name='theme-color' content=themeColor)
|
meta(name='theme-color' content=themeColor)
|
||||||
meta(name='referrer' content='origin')
|
meta(name='referrer' content='origin')
|
||||||
|
link(rel='manifest' href='/manifest.json')
|
||||||
|
|
||||||
title Misskey
|
title Misskey
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,9 @@
|
||||||
// misskey.alice => misskey
|
// misskey.alice => misskey
|
||||||
// misskey.strawberry.pasta => misskey
|
// misskey.strawberry.pasta => misskey
|
||||||
// dev.misskey.arisu.tachibana => dev
|
// dev.misskey.arisu.tachibana => dev
|
||||||
let app = url.host.split('.')[0];
|
let app = url.host == 'localhost'
|
||||||
|
? 'misskey'
|
||||||
|
: url.host.split('.')[0];
|
||||||
|
|
||||||
// Detect the user language
|
// Detect the user language
|
||||||
// Note: The default language is English
|
// Note: The default language is English
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as riot from 'riot';
|
import * as riot from 'riot';
|
||||||
const route = require('page');
|
import * as route from 'page';
|
||||||
let page = null;
|
let page = null;
|
||||||
|
|
||||||
export default me => {
|
export default () => {
|
||||||
route('/', index);
|
route('/', index);
|
||||||
route('/:channel', channel);
|
route('/:channel', channel);
|
||||||
route('*', notFound);
|
route('*', notFound);
|
||||||
|
@ -22,7 +22,7 @@ export default me => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXEC
|
// EXEC
|
||||||
route();
|
(route as any)();
|
||||||
};
|
};
|
||||||
|
|
||||||
function mount(content) {
|
function mount(content) {
|
|
@ -12,7 +12,7 @@ import route from './router';
|
||||||
/**
|
/**
|
||||||
* init
|
* init
|
||||||
*/
|
*/
|
||||||
init(me => {
|
init(() => {
|
||||||
// Start routing
|
// Start routing
|
||||||
route(me);
|
route();
|
||||||
});
|
});
|
|
@ -26,11 +26,11 @@
|
||||||
<hr>
|
<hr>
|
||||||
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
||||||
<div if={ !SIGNIN }>
|
<div if={ !SIGNIN }>
|
||||||
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
|
<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<footer>
|
<footer>
|
||||||
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
|
<small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
<style>
|
<style>
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import Progress from '../../common/scripts/loading';
|
import Progress from '../../common/scripts/loading';
|
||||||
import ChannelStream from '../../common/scripts/channel-stream';
|
import ChannelStream from '../../common/scripts/streaming/channel-stream';
|
||||||
|
|
||||||
this.mixin('i');
|
this.mixin('i');
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
@ -66,7 +66,6 @@
|
||||||
this.channel = null;
|
this.channel = null;
|
||||||
this.posts = null;
|
this.posts = null;
|
||||||
this.connection = new ChannelStream(this.id);
|
this.connection = new ChannelStream(this.id);
|
||||||
this.version = VERSION;
|
|
||||||
this.unreadCount = 0;
|
this.unreadCount = 0;
|
||||||
|
|
||||||
this.on('mount', () => {
|
this.on('mount', () => {
|
||||||
|
@ -166,7 +165,7 @@
|
||||||
<mk-channel-post>
|
<mk-channel-post>
|
||||||
<header>
|
<header>
|
||||||
<a class="index" onclick={ reply }>{ post.index }:</a>
|
<a class="index" onclick={ reply }>{ post.index }:</a>
|
||||||
<a class="name" href={ CONFIG.url + '/' + post.user.username }><b>{ post.user.name }</b></a>
|
<a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
|
||||||
<mk-time time={ post.created_at }/>
|
<mk-time time={ post.created_at }/>
|
||||||
<mk-time time={ post.created_at } mode="detail"/>
|
<mk-time time={ post.created_at } mode="detail"/>
|
||||||
<span>ID:<i>{ post.user.username }</i></span>
|
<span>ID:<i>{ post.user.username }</i></span>
|
||||||
|
@ -284,8 +283,6 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import CONFIG from '../../common/scripts/config';
|
|
||||||
|
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
||||||
this.channel = this.opts.channel;
|
this.channel = this.opts.channel;
|
||||||
|
@ -343,7 +340,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeFile = () => {
|
this.changeFile = () => {
|
||||||
this.refs.file.files.forEach(this.upload);
|
Array.from(this.refs.file.files).forEach(this.upload);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.selectFile = () => {
|
this.selectFile = () => {
|
||||||
|
@ -357,7 +354,7 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.open(CONFIG.url + '/selectdrive?multiple=true',
|
window.open(_URL_ + '/selectdrive?multiple=true',
|
||||||
'drive_window',
|
'drive_window',
|
||||||
'height=500,width=800');
|
'height=500,width=800');
|
||||||
};
|
};
|
||||||
|
@ -367,7 +364,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onpaste = e => {
|
this.onpaste = e => {
|
||||||
e.clipboardData.items.forEach(item => {
|
Array.from(e.clipboardData.items).forEach(item => {
|
||||||
if (item.kind == 'file') {
|
if (item.kind == 'file') {
|
||||||
this.upload(item.getAsFile());
|
this.upload(item.getAsFile());
|
||||||
}
|
}
|
||||||
|
@ -390,7 +387,7 @@
|
||||||
</mk-twitter-button>
|
</mk-twitter-button>
|
||||||
|
|
||||||
<mk-line-button>
|
<mk-line-button>
|
||||||
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div>
|
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
|
||||||
<script>
|
<script>
|
||||||
this.on('mount', () => {
|
this.on('mount', () => {
|
||||||
const head = document.getElementsByTagName('head')[0];
|
const head = document.getElementsByTagName('head')[0];
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<mk-header>
|
<mk-header>
|
||||||
<div>
|
<div>
|
||||||
<a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a>
|
<a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
|
<a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
|
||||||
<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
|
<a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
||||||
this.on('mount', () => {
|
this.on('mount', () => {
|
||||||
this.api('channels').then(channels => {
|
this.api('channels', {
|
||||||
|
limit: 100
|
||||||
|
}).then(channels => {
|
||||||
this.update({
|
this.update({
|
||||||
channels: channels
|
channels: channels
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,351 @@
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import * as riot from 'riot';
|
||||||
|
import signout from './scripts/signout';
|
||||||
|
import Progress from './scripts/loading';
|
||||||
|
import HomeStreamManager from './scripts/streaming/home-stream-manager';
|
||||||
|
import api from './scripts/api';
|
||||||
|
|
||||||
|
//#region environment variables
|
||||||
|
declare const _VERSION_: string;
|
||||||
|
declare const _LANG_: string;
|
||||||
|
declare const _API_URL_: string;
|
||||||
|
declare const _SW_PUBLICKEY_: string;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misskey Operating System
|
||||||
|
*/
|
||||||
|
export default class MiOS extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Misskeyの /meta で取得できるメタ情報
|
||||||
|
*/
|
||||||
|
private meta: {
|
||||||
|
data: { [x: string]: any };
|
||||||
|
chachedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
private isMetaFetching = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signing user
|
||||||
|
*/
|
||||||
|
public i: { [x: string]: any };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether signed in
|
||||||
|
*/
|
||||||
|
public get isSignedin() {
|
||||||
|
return this.i != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether is debug mode
|
||||||
|
*/
|
||||||
|
public get debug() {
|
||||||
|
return localStorage.getItem('debug') == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection manager of home stream
|
||||||
|
*/
|
||||||
|
public stream: HomeStreamManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registration of service worker
|
||||||
|
*/
|
||||||
|
private swRegistration: ServiceWorkerRegistration = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether should register ServiceWorker
|
||||||
|
*/
|
||||||
|
private shouldRegisterSw: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MiOSインスタンスを作成します
|
||||||
|
* @param shouldRegisterSw ServiceWorkerを登録するかどうか
|
||||||
|
*/
|
||||||
|
constructor(shouldRegisterSw = false) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.shouldRegisterSw = shouldRegisterSw;
|
||||||
|
|
||||||
|
//#region BIND
|
||||||
|
this.log = this.log.bind(this);
|
||||||
|
this.logInfo = this.logInfo.bind(this);
|
||||||
|
this.logWarn = this.logWarn.bind(this);
|
||||||
|
this.logError = this.logError.bind(this);
|
||||||
|
this.init = this.init.bind(this);
|
||||||
|
this.api = this.api.bind(this);
|
||||||
|
this.getMeta = this.getMeta.bind(this);
|
||||||
|
this.registerSw = this.registerSw.bind(this);
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(...args) {
|
||||||
|
if (!this.debug) return;
|
||||||
|
console.log.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logInfo(...args) {
|
||||||
|
if (!this.debug) return;
|
||||||
|
console.info.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logWarn(...args) {
|
||||||
|
if (!this.debug) return;
|
||||||
|
console.warn.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logError(...args) {
|
||||||
|
if (!this.debug) return;
|
||||||
|
console.error.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize MiOS (boot)
|
||||||
|
* @param callback A function that call when initialized
|
||||||
|
*/
|
||||||
|
public async init(callback) {
|
||||||
|
// ユーザーをフェッチしてコールバックする
|
||||||
|
const fetchme = (token, cb) => {
|
||||||
|
let me = null;
|
||||||
|
|
||||||
|
// Return when not signed in
|
||||||
|
if (token == null) {
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user
|
||||||
|
fetch(`${_API_URL_}/i`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
i: token
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// When success
|
||||||
|
.then(res => {
|
||||||
|
// When failed to authenticate user
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return signout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
res.json().then(i => {
|
||||||
|
me = i;
|
||||||
|
me.token = token;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// When failure
|
||||||
|
.catch(() => {
|
||||||
|
// Render the error screen
|
||||||
|
document.body.innerHTML = '<mk-error />';
|
||||||
|
riot.mount('*');
|
||||||
|
|
||||||
|
Progress.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
if (cb) cb(me);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// フェッチが完了したとき
|
||||||
|
const fetched = me => {
|
||||||
|
if (me) {
|
||||||
|
riot.observable(me);
|
||||||
|
|
||||||
|
// この me オブジェクトを更新するメソッド
|
||||||
|
me.update = data => {
|
||||||
|
if (data) Object.assign(me, data);
|
||||||
|
me.trigger('updated');
|
||||||
|
};
|
||||||
|
|
||||||
|
// ローカルストレージにキャッシュ
|
||||||
|
localStorage.setItem('me', JSON.stringify(me));
|
||||||
|
|
||||||
|
// 自分の情報が更新されたとき
|
||||||
|
me.on('updated', () => {
|
||||||
|
// キャッシュ更新
|
||||||
|
localStorage.setItem('me', JSON.stringify(me));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.i = me;
|
||||||
|
|
||||||
|
// Init home stream manager
|
||||||
|
this.stream = this.isSignedin
|
||||||
|
? new HomeStreamManager(this.i)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Finish init
|
||||||
|
callback();
|
||||||
|
|
||||||
|
//#region Post
|
||||||
|
|
||||||
|
// Init service worker
|
||||||
|
if (this.shouldRegisterSw) this.registerSw();
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get cached account data
|
||||||
|
const cachedMe = JSON.parse(localStorage.getItem('me'));
|
||||||
|
|
||||||
|
// キャッシュがあったとき
|
||||||
|
if (cachedMe) {
|
||||||
|
// とりあえずキャッシュされたデータでお茶を濁して(?)おいて、
|
||||||
|
fetched(cachedMe);
|
||||||
|
|
||||||
|
// 後から新鮮なデータをフェッチ
|
||||||
|
fetchme(cachedMe.token, freshData => {
|
||||||
|
Object.assign(cachedMe, freshData);
|
||||||
|
cachedMe.trigger('updated');
|
||||||
|
cachedMe.trigger('refreshed');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get token from cookie
|
||||||
|
const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
|
||||||
|
|
||||||
|
fetchme(i, fetched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register service worker
|
||||||
|
*/
|
||||||
|
private registerSw() {
|
||||||
|
// Check whether service worker and push manager supported
|
||||||
|
const isSwSupported =
|
||||||
|
('serviceWorker' in navigator) && ('PushManager' in window);
|
||||||
|
|
||||||
|
// Reject when browser not service worker supported
|
||||||
|
if (!isSwSupported) return;
|
||||||
|
|
||||||
|
// Reject when not signed in to Misskey
|
||||||
|
if (!this.isSignedin) return;
|
||||||
|
|
||||||
|
// When service worker activated
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
this.log('[sw] ready: ', registration);
|
||||||
|
|
||||||
|
this.swRegistration = registration;
|
||||||
|
|
||||||
|
// Options of pushManager.subscribe
|
||||||
|
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||||
|
const opts = {
|
||||||
|
// A boolean indicating that the returned push subscription
|
||||||
|
// will only be used for messages whose effect is made visible to the user.
|
||||||
|
userVisibleOnly: true,
|
||||||
|
|
||||||
|
// A public key your push server will use to send
|
||||||
|
// messages to client apps via a push server.
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(_SW_PUBLICKEY_)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe push notification
|
||||||
|
this.swRegistration.pushManager.subscribe(opts).then(subscription => {
|
||||||
|
this.log('[sw] Subscribe OK:', subscription);
|
||||||
|
|
||||||
|
function encode(buffer: ArrayBuffer) {
|
||||||
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register
|
||||||
|
this.api('sw/register', {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
auth: encode(subscription.getKey('auth')),
|
||||||
|
publickey: encode(subscription.getKey('p256dh'))
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// When subscribe failed
|
||||||
|
.catch(async (err: Error) => {
|
||||||
|
this.logError('[sw] Subscribe Error:', err);
|
||||||
|
|
||||||
|
// 通知が許可されていなかったとき
|
||||||
|
if (err.name == 'NotAllowedError') {
|
||||||
|
this.logError('[sw] Subscribe failed due to notification not allowed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
|
||||||
|
// 既に存在していることが原因でエラーになった可能性があるので、
|
||||||
|
// そのサブスクリプションを解除しておく
|
||||||
|
const subscription = await this.swRegistration.pushManager.getSubscription();
|
||||||
|
if (subscription) subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// The path of service worker script
|
||||||
|
const sw = `/sw.${_VERSION_}.${_LANG_}.js`;
|
||||||
|
|
||||||
|
// Register service worker
|
||||||
|
navigator.serviceWorker.register(sw).then(registration => {
|
||||||
|
// 登録成功
|
||||||
|
this.logInfo('[sw] Registration successful with scope: ', registration.scope);
|
||||||
|
}).catch(err => {
|
||||||
|
// 登録失敗 :(
|
||||||
|
this.logError('[sw] Registration failed: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misskey APIにリクエストします
|
||||||
|
* @param endpoint エンドポイント名
|
||||||
|
* @param data パラメータ
|
||||||
|
*/
|
||||||
|
public api(endpoint: string, data?: { [x: string]: any }) {
|
||||||
|
return api(this.i, endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misskeyのメタ情報を取得します
|
||||||
|
* @param force キャッシュを無視するか否か
|
||||||
|
*/
|
||||||
|
public getMeta(force = false) {
|
||||||
|
return new Promise<{ [x: string]: any }>(async (res, rej) => {
|
||||||
|
if (this.isMetaFetching) {
|
||||||
|
this.once('_meta_fetched_', () => {
|
||||||
|
res(this.meta.data);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expire = 1000 * 60; // 1min
|
||||||
|
|
||||||
|
// forceが有効, meta情報を保持していない or 期限切れ
|
||||||
|
if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) {
|
||||||
|
this.isMetaFetching = true;
|
||||||
|
const meta = await this.api('meta');
|
||||||
|
this.meta = {
|
||||||
|
data: meta,
|
||||||
|
chachedAt: new Date()
|
||||||
|
};
|
||||||
|
this.isMetaFetching = false;
|
||||||
|
this.emit('_meta_fetched_');
|
||||||
|
res(meta);
|
||||||
|
} else {
|
||||||
|
res(this.meta.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the URL safe base64 string to a Uint8Array
|
||||||
|
* @param base64String base64 string
|
||||||
|
*/
|
||||||
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import * as riot from 'riot';
|
||||||
|
|
||||||
|
import MiOS from './mios';
|
||||||
|
import ServerStreamManager from './scripts/streaming/server-stream-manager';
|
||||||
|
import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
|
||||||
|
import MessagingIndexStreamManager from './scripts/streaming/messaging-index-stream-manager';
|
||||||
|
import DriveStreamManager from './scripts/streaming/drive-stream-manager';
|
||||||
|
|
||||||
|
export default (mios: MiOS) => {
|
||||||
|
(riot as any).mixin('os', {
|
||||||
|
mios: mios
|
||||||
|
});
|
||||||
|
|
||||||
|
(riot as any).mixin('i', {
|
||||||
|
init: function() {
|
||||||
|
this.I = mios.i;
|
||||||
|
this.SIGNIN = mios.isSignedin;
|
||||||
|
|
||||||
|
if (this.SIGNIN) {
|
||||||
|
this.on('mount', () => {
|
||||||
|
mios.i.on('updated', this.update);
|
||||||
|
});
|
||||||
|
this.on('unmount', () => {
|
||||||
|
mios.i.off('updated', this.update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
me: mios.i
|
||||||
|
});
|
||||||
|
|
||||||
|
(riot as any).mixin('api', {
|
||||||
|
api: mios.api
|
||||||
|
});
|
||||||
|
|
||||||
|
(riot as any).mixin('stream', { stream: mios.stream });
|
||||||
|
(riot as any).mixin('drive-stream', { driveStream: new DriveStreamManager(mios.i) });
|
||||||
|
(riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
|
||||||
|
(riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
|
||||||
|
(riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStreamManager(mios.i) });
|
||||||
|
};
|
|
@ -1,8 +0,0 @@
|
||||||
import * as riot from 'riot';
|
|
||||||
import api from '../scripts/api';
|
|
||||||
|
|
||||||
export default me => {
|
|
||||||
riot.mixin('api', {
|
|
||||||
api: api.bind(null, me ? me.token : null)
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import * as riot from 'riot';
|
|
||||||
|
|
||||||
export default me => {
|
|
||||||
riot.mixin('i', {
|
|
||||||
init: function() {
|
|
||||||
this.I = me;
|
|
||||||
this.SIGNIN = me != null;
|
|
||||||
|
|
||||||
if (this.SIGNIN) {
|
|
||||||
this.on('mount', () => {
|
|
||||||
me.on('updated', this.update);
|
|
||||||
});
|
|
||||||
this.on('unmount', () => {
|
|
||||||
me.off('updated', this.update);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
me: me
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,9 +0,0 @@
|
||||||
import activateMe from './i';
|
|
||||||
import activateApi from './api';
|
|
||||||
import activateStream from './stream';
|
|
||||||
|
|
||||||
export default (me, stream) => {
|
|
||||||
activateMe(me);
|
|
||||||
activateApi(me);
|
|
||||||
activateStream(stream);
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
import * as riot from 'riot';
|
|
||||||
|
|
||||||
export default stream => {
|
|
||||||
riot.mixin('stream', { stream });
|
|
||||||
};
|
|
|
@ -2,7 +2,7 @@
|
||||||
* API Request
|
* API Request
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CONFIG from './config';
|
declare const _API_URL_: string;
|
||||||
|
|
||||||
let spinner = null;
|
let spinner = null;
|
||||||
let pending = 0;
|
let pending = 0;
|
||||||
|
@ -14,7 +14,7 @@ let pending = 0;
|
||||||
* @param {any} [data={}] Data
|
* @param {any} [data={}] Data
|
||||||
* @return {Promise<any>} Response
|
* @return {Promise<any>} Response
|
||||||
*/
|
*/
|
||||||
export default (i, endpoint, data = {}) => {
|
export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
|
||||||
if (++pending === 1) {
|
if (++pending === 1) {
|
||||||
spinner = document.createElement('div');
|
spinner = document.createElement('div');
|
||||||
spinner.setAttribute('id', 'wait');
|
spinner.setAttribute('id', 'wait');
|
||||||
|
@ -22,11 +22,11 @@ export default (i, endpoint, data = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the credential
|
// Append the credential
|
||||||
if (i != null) data.i = typeof i === 'object' ? i.token : i;
|
if (i != null) (data as any).i = typeof i === 'object' ? i.token : i;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Send request
|
// Send request
|
||||||
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${CONFIG.apiUrl}/${endpoint}`, {
|
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${_API_URL_}/${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
credentials: endpoint === 'signin' ? 'include' : 'omit'
|
credentials: endpoint === 'signin' ? 'include' : 'omit'
|
|
@ -1,6 +1,6 @@
|
||||||
export default (bytes, digits = 0) => {
|
export default (bytes, digits = 0) => {
|
||||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
if (bytes == 0) return '0Byte';
|
if (bytes == 0) return '0Byte';
|
||||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
||||||
};
|
};
|
|
@ -1,14 +0,0 @@
|
||||||
import CONFIG from './config';
|
|
||||||
|
|
||||||
export default function() {
|
|
||||||
fetch(CONFIG.apiUrl + '/meta', {
|
|
||||||
method: 'POST'
|
|
||||||
}).then(res => {
|
|
||||||
res.json().then(meta => {
|
|
||||||
if (meta.version != VERSION) {
|
|
||||||
localStorage.setItem('should-refresh', 'true');
|
|
||||||
alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import MiOS from '../mios';
|
||||||
|
|
||||||
|
declare const _VERSION_: string;
|
||||||
|
|
||||||
|
export default async function(mios: MiOS) {
|
||||||
|
const meta = await mios.getMeta();
|
||||||
|
|
||||||
|
if (meta.version != _VERSION_) {
|
||||||
|
localStorage.setItem('should-refresh', 'true');
|
||||||
|
alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', _VERSION_));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import getPostSummary from '../../../../common/get-post-summary';
|
||||||
|
import getReactionEmoji from '../../../../common/get-reaction-emoji';
|
||||||
|
|
||||||
|
type Notification = {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon: string;
|
||||||
|
onclick?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: i18n
|
||||||
|
|
||||||
|
export default function(type, data): Notification {
|
||||||
|
switch (type) {
|
||||||
|
case 'drive_file_created':
|
||||||
|
return {
|
||||||
|
title: 'ファイルがアップロードされました',
|
||||||
|
body: data.name,
|
||||||
|
icon: data.url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'mention':
|
||||||
|
return {
|
||||||
|
title: `${data.user.name}さんから:`,
|
||||||
|
body: getPostSummary(data),
|
||||||
|
icon: data.user.avatar_url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'reply':
|
||||||
|
return {
|
||||||
|
title: `${data.user.name}さんから返信:`,
|
||||||
|
body: getPostSummary(data),
|
||||||
|
icon: data.user.avatar_url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'quote':
|
||||||
|
return {
|
||||||
|
title: `${data.user.name}さんが引用:`,
|
||||||
|
body: getPostSummary(data),
|
||||||
|
icon: data.user.avatar_url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'reaction':
|
||||||
|
return {
|
||||||
|
title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
|
||||||
|
body: getPostSummary(data.post),
|
||||||
|
icon: data.user.avatar_url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'unread_messaging_message':
|
||||||
|
return {
|
||||||
|
title: `${data.user.name}さんからメッセージ:`,
|
||||||
|
body: data.text, // TODO: getMessagingMessageSummary(data),
|
||||||
|
icon: data.user.avatar_url + '?thumbnail&size=64'
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue