mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-27 16:06:44 -06:00
Merge branch 'master' of github.com:syuilo/misskey
This commit is contained in:
commit
6c75bc6d51
275 changed files with 7433 additions and 2804 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,7 +2,6 @@
|
|||
/.vscode
|
||||
/node_modules
|
||||
/built
|
||||
/uploads
|
||||
/data
|
||||
npm-debug.log
|
||||
*.pem
|
||||
|
|
|
@ -22,5 +22,5 @@ elasticsearch:
|
|||
port: 9200
|
||||
pass: ''
|
||||
recaptcha:
|
||||
siteKey: hima
|
||||
secretKey: saku
|
||||
site_key: hima
|
||||
secret_key: saku
|
||||
|
|
|
@ -22,5 +22,5 @@ elasticsearch:
|
|||
port: 9200
|
||||
pass: ''
|
||||
recaptcha:
|
||||
siteKey: hima
|
||||
secretKey: saku
|
||||
site_key: hima
|
||||
secret_key: saku
|
||||
|
|
213
CHANGELOG.md
213
CHANGELOG.md
|
@ -2,6 +2,219 @@ ChangeLog (Release Notes)
|
|||
=========================
|
||||
主に 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)
|
||||
-----------------
|
||||
* いい感じに
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
DONORS
|
||||
======
|
||||
The list of people who have sent donation for Misskey.
|
||||
|
||||
(no particular order)
|
||||
|
||||
|
@ -7,12 +8,14 @@ DONORS
|
|||
* 俺様
|
||||
* なぎうり
|
||||
* スルメ https://surume.tk/
|
||||
* 藍
|
||||
* 音船 https://otofune.me/
|
||||
|
||||
: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].
|
||||
|
||||
|
|
22
README.md
22
README.md
|
@ -17,7 +17,7 @@ Key features
|
|||
* Automatically updated timeline
|
||||
* Private messages
|
||||
* Free 1GB storage for each all users
|
||||
* Machine learning
|
||||
* ServiceWorker support
|
||||
* Web API for third-party applications
|
||||
* No ads
|
||||
|
||||
|
@ -38,10 +38,18 @@ Please see [ChangeLog](./CHANGELOG.md).
|
|||
|
||||
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].
|
||||
|
||||
**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
|
||||
----------------------------------------------------------------
|
||||
|
@ -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
|
||||
[travis-link]: https://travis-ci.org/syuilo/misskey
|
||||
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
|
||||
[dependencies-link]: https://gemnasium.com/syuilo/misskey
|
||||
[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square
|
||||
[dependencies-link]: https://david-dm.org/syuilo/misskey
|
||||
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
|
||||
[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
|
||||
[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 -->
|
||||
[syuilo-link]: https://syuilo.com
|
||||
[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
|
52
docs/config.md
Normal file
52
docs/config.md
Normal file
|
@ -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
|
||||
================================================================
|
||||
|
||||
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.
|
||||
|
||||
[Japanese version also available - 日本語版もあります](./setup.ja.md)
|
||||
|
@ -36,6 +36,15 @@ Note that Misskey uses following subdomains:
|
|||
Misskey requires reCAPTCHA tokens.
|
||||
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
|
||||
----------------------------------------------------------------
|
||||
Please install and setup these softwares:
|
||||
|
@ -51,24 +60,6 @@ Please install and setup these softwares:
|
|||
|
||||
*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`
|
||||
2. `cd misskey`
|
||||
|
|
|
@ -37,6 +37,15 @@ Misskeyは以下のサブドメインを使います:
|
|||
MisskeyはreCAPTCHAトークンを必要とします。
|
||||
https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。
|
||||
|
||||
*(オプション)* VAPIDキーペアの生成
|
||||
----------------------------------------------------------------
|
||||
ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
|
||||
|
||||
``` shell
|
||||
npm install web-push -g
|
||||
web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
*3.* 依存関係をインストールする
|
||||
----------------------------------------------------------------
|
||||
これらのソフトウェアをインストール・設定してください:
|
||||
|
@ -52,26 +61,6 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生
|
|||
|
||||
*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`
|
||||
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 pug = require('gulp-pug');
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as chalk from 'chalk';
|
||||
import chalk from 'chalk';
|
||||
import imagemin = require('gulp-imagemin');
|
||||
import * as rename from 'gulp-rename';
|
||||
import * as mocha from 'gulp-mocha';
|
||||
|
@ -81,9 +81,19 @@ gulp.task('lint', () =>
|
|||
.pipe(tslint.report())
|
||||
);
|
||||
|
||||
gulp.task('format', () =>
|
||||
gulp.src('./src/**/*.ts')
|
||||
.pipe(tslint({
|
||||
formatter: 'verbose',
|
||||
fix: true
|
||||
}))
|
||||
.pipe(tslint.report())
|
||||
);
|
||||
|
||||
gulp.task('mocha', () =>
|
||||
gulp.src([])
|
||||
.pipe(mocha({
|
||||
exit: true
|
||||
//compilers: 'ts:ts-node/register'
|
||||
} as any))
|
||||
);
|
||||
|
@ -123,7 +133,7 @@ gulp.task('build:client:script', () =>
|
|||
.pipe(replace('VERSION', JSON.stringify(version)))
|
||||
.pipe(isProduction ? uglify({
|
||||
toplevel: true
|
||||
}) : gutil.noop())
|
||||
} as any) : gutil.noop())
|
||||
.pipe(gulp.dest('./built/web/assets/')) as any
|
||||
);
|
||||
|
||||
|
|
|
@ -13,6 +13,15 @@ common:
|
|||
months_ago: "{}month(s) ago"
|
||||
years_ago: "{}year(s) ago"
|
||||
|
||||
weekday-short:
|
||||
sunday: "S"
|
||||
monday: "M"
|
||||
tuesday: "T"
|
||||
wednesday: "W"
|
||||
thursday: "T"
|
||||
friday: "F"
|
||||
satruday: "S"
|
||||
|
||||
reactions:
|
||||
like: "Like"
|
||||
love: "Love"
|
||||
|
@ -41,6 +50,15 @@ common:
|
|||
my-token-regenerated: "Your token is just regenerated, so you will signout."
|
||||
|
||||
tags:
|
||||
mk-nav-links:
|
||||
about: "About"
|
||||
stats: "Stats"
|
||||
status: "Status"
|
||||
wiki: "Wiki"
|
||||
donors: "Donors"
|
||||
repository: "Repository"
|
||||
develop: "Developers"
|
||||
|
||||
mk-messaging-form:
|
||||
attach-from-local: "Attach file from your pc"
|
||||
attach-from-drive: "Attach file from the drive"
|
||||
|
@ -225,7 +243,6 @@ desktop:
|
|||
mk-drive-browser-file:
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
wallpaper: "Wallpaper"
|
||||
|
||||
mk-drive-browser-folder-contextmenu:
|
||||
move-to-this-folder: "Move to this folder"
|
||||
|
@ -242,14 +259,11 @@ desktop:
|
|||
mk-drive-browser-nav-folder:
|
||||
drive: "Drive"
|
||||
|
||||
mk-nav-home-widget:
|
||||
about: "About"
|
||||
stats: "Stats"
|
||||
status: "Status"
|
||||
wiki: "Wiki"
|
||||
donors: "Donors"
|
||||
repository: "Repository"
|
||||
develop: "Developers"
|
||||
mk-selectdrive-page:
|
||||
title: "Choose a file(s)"
|
||||
ok: "OK"
|
||||
cancel: "Cancel"
|
||||
upload: "Upload a file(s) from you PC"
|
||||
|
||||
mk-ui-header-nav:
|
||||
home: "Home"
|
||||
|
@ -267,6 +281,12 @@ desktop:
|
|||
settings: "Settings"
|
||||
signout: "Sign out"
|
||||
|
||||
mk-ui-header-post-button:
|
||||
post: "Compose new Post"
|
||||
|
||||
mk-ui-header-notifications:
|
||||
title: "Notifications"
|
||||
|
||||
mk-password-setting:
|
||||
reset: "Change your password"
|
||||
enter-current-password: "Enter the current password"
|
||||
|
@ -327,7 +347,7 @@ desktop:
|
|||
title: "Server info"
|
||||
toggle: "Toggle views"
|
||||
|
||||
mk-activity-home-widget:
|
||||
mk-activity-widget:
|
||||
title: "Activity"
|
||||
toggle: "Toggle views"
|
||||
|
||||
|
@ -354,6 +374,34 @@ desktop:
|
|||
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!"
|
||||
|
||||
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:
|
||||
quote: "Quote..."
|
||||
cancel: "Cancel"
|
||||
|
@ -365,6 +413,24 @@ desktop:
|
|||
mk-repost-form-window:
|
||||
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:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
|
@ -374,7 +440,7 @@ mobile:
|
|||
download: "Download"
|
||||
rename: "Rename"
|
||||
move: "Move"
|
||||
hash: "Hash"
|
||||
hash: "Hash (md5)"
|
||||
|
||||
mk-entrance-signin:
|
||||
signup: "Sign up"
|
||||
|
|
|
@ -13,6 +13,15 @@ common:
|
|||
months_ago: "{}ヶ月前"
|
||||
years_ago: "{}年前"
|
||||
|
||||
weekday-short:
|
||||
sunday: "日"
|
||||
monday: "月"
|
||||
tuesday: "火"
|
||||
wednesday: "水"
|
||||
thursday: "木"
|
||||
friday: "金"
|
||||
satruday: "土"
|
||||
|
||||
reactions:
|
||||
like: "いいね"
|
||||
love: "ハート"
|
||||
|
@ -41,6 +50,15 @@ common:
|
|||
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
|
||||
|
||||
tags:
|
||||
mk-nav-links:
|
||||
about: "Misskeyについて"
|
||||
stats: "統計"
|
||||
status: "ステータス"
|
||||
wiki: "Wiki"
|
||||
donors: "ドナー"
|
||||
repository: "リポジトリ"
|
||||
develop: "開発者"
|
||||
|
||||
mk-messaging-form:
|
||||
attach-from-local: "PCからファイルを添付する"
|
||||
attach-from-drive: "ドライブからファイルを添付する"
|
||||
|
@ -225,7 +243,6 @@ desktop:
|
|||
mk-drive-browser-file:
|
||||
avatar: "アバター"
|
||||
banner: "バナー"
|
||||
wallpaper: "壁紙"
|
||||
|
||||
mk-drive-browser-folder-contextmenu:
|
||||
move-to-this-folder: "このフォルダへ移動"
|
||||
|
@ -242,14 +259,11 @@ desktop:
|
|||
mk-drive-browser-nav-folder:
|
||||
drive: "ドライブ"
|
||||
|
||||
mk-nav-home-widget:
|
||||
about: "Misskeyについて"
|
||||
stats: "統計"
|
||||
status: "ステータス"
|
||||
wiki: "Wiki"
|
||||
donors: "ドナー"
|
||||
repository: "リポジトリ"
|
||||
develop: "開発者"
|
||||
mk-selectdrive-page:
|
||||
title: "ファイルを選択してください"
|
||||
ok: "決定"
|
||||
cancel: "キャンセル"
|
||||
upload: "PCからドライブにファイルをアップロード"
|
||||
|
||||
mk-ui-header-nav:
|
||||
home: "ホーム"
|
||||
|
@ -267,6 +281,12 @@ desktop:
|
|||
settings: "設定"
|
||||
signout: "サインアウト"
|
||||
|
||||
mk-ui-header-post-button:
|
||||
post: "新規投稿"
|
||||
|
||||
mk-ui-header-notifications:
|
||||
title: "通知"
|
||||
|
||||
mk-password-setting:
|
||||
reset: "パスワードを変更する"
|
||||
enter-current-password: "現在のパスワードを入力してください"
|
||||
|
@ -327,7 +347,7 @@ desktop:
|
|||
title: "サーバー情報"
|
||||
toggle: "表示を切り替え"
|
||||
|
||||
mk-activity-home-widget:
|
||||
mk-activity-widget:
|
||||
title: "アクティビティ"
|
||||
toggle: "表示を切り替え"
|
||||
|
||||
|
@ -354,6 +374,34 @@ desktop:
|
|||
title: "寄付のお願い"
|
||||
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:
|
||||
quote: "引用する..."
|
||||
cancel: "キャンセル"
|
||||
|
@ -365,6 +413,24 @@ desktop:
|
|||
mk-repost-form-window:
|
||||
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:
|
||||
tags:
|
||||
mk-selectdrive-page:
|
||||
|
@ -374,7 +440,7 @@ mobile:
|
|||
download: "ダウンロード"
|
||||
rename: "名前を変更"
|
||||
move: "移動"
|
||||
hash: "ハッシュ"
|
||||
hash: "ハッシュ (md5)"
|
||||
|
||||
mk-entrance-signin:
|
||||
signup: "新規登録"
|
||||
|
|
320
package.json
320
package.json
|
@ -1,158 +1,166 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "0.0.2807",
|
||||
"license": "MIT",
|
||||
"description": "A miniblog-based SNS",
|
||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||
"repository": "https://github.com/syuilo/misskey.git",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"config": "node ./tools/init.js",
|
||||
"start": "node ./built",
|
||||
"debug": "DEBUG=misskey:* node ./built",
|
||||
"swagger": "node ./swagger.js",
|
||||
"build": "gulp build",
|
||||
"rebuild": "gulp rebuild",
|
||||
"clean": "gulp clean",
|
||||
"cleanall": "gulp cleanall",
|
||||
"lint": "gulp lint",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "2.4.0",
|
||||
"@types/body-parser": "1.16.5",
|
||||
"@types/chai": "4.0.4",
|
||||
"@types/chai-http": "3.0.3",
|
||||
"@types/chalk": "0.4.31",
|
||||
"@types/compression": "0.0.34",
|
||||
"@types/cors": "2.8.1",
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/elasticsearch": "5.0.14",
|
||||
"@types/event-stream": "3.3.32",
|
||||
"@types/express": "4.0.37",
|
||||
"@types/gm": "1.17.32",
|
||||
"@types/gulp": "4.0.3",
|
||||
"@types/gulp-htmlmin": "1.3.30",
|
||||
"@types/gulp-mocha": "0.0.30",
|
||||
"@types/gulp-rename": "0.0.32",
|
||||
"@types/gulp-replace": "0.0.30",
|
||||
"@types/gulp-tslint": "3.6.31",
|
||||
"@types/gulp-typescript": "2.13.0",
|
||||
"@types/gulp-uglify": "0.0.30",
|
||||
"@types/gulp-util": "3.0.31",
|
||||
"@types/inquirer": "0.0.34",
|
||||
"@types/is-root": "1.0.0",
|
||||
"@types/is-url": "1.2.28",
|
||||
"@types/js-yaml": "3.9.0",
|
||||
"@types/mocha": "2.2.43",
|
||||
"@types/mongodb": "2.2.13",
|
||||
"@types/monk": "1.0.6",
|
||||
"@types/morgan": "1.7.33",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/multer": "1.3.2",
|
||||
"@types/node": "8.0.33",
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.6.0",
|
||||
"@types/request": "2.0.4",
|
||||
"@types/rimraf": "2.0.0",
|
||||
"@types/riot": "3.6.0",
|
||||
"@types/serve-favicon": "2.2.28",
|
||||
"@types/uuid": "3.4.2",
|
||||
"@types/webpack": "3.0.13",
|
||||
"@types/webpack-stream": "3.2.7",
|
||||
"@types/websocket": "0.0.34",
|
||||
"awesome-typescript-loader": "3.2.3",
|
||||
"chai": "4.1.2",
|
||||
"chai-http": "3.0.0",
|
||||
"css-loader": "0.28.7",
|
||||
"event-stream": "3.3.4",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cssnano": "2.1.2",
|
||||
"gulp-htmlmin": "3.0.0",
|
||||
"gulp-imagemin": "3.4.0",
|
||||
"gulp-mocha": "4.3.1",
|
||||
"gulp-pug": "3.3.0",
|
||||
"gulp-rename": "1.2.2",
|
||||
"gulp-replace": "0.6.1",
|
||||
"gulp-tslint": "8.1.2",
|
||||
"gulp-typescript": "3.2.2",
|
||||
"gulp-uglify": "3.0.0",
|
||||
"gulp-util": "3.0.8",
|
||||
"mocha": "3.5.3",
|
||||
"riot-tag-loader": "1.0.0",
|
||||
"string-replace-webpack-plugin": "0.1.3",
|
||||
"style-loader": "0.19.0",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.1",
|
||||
"swagger-jsdoc": "1.9.7",
|
||||
"tslint": "5.7.0",
|
||||
"uglify-es": "3.0.27",
|
||||
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
|
||||
"uglifyjs-webpack-plugin": "1.0.0-beta.2",
|
||||
"webpack": "3.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"accesses": "2.5.0",
|
||||
"animejs": "2.2.0",
|
||||
"autwh": "0.0.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.18.2",
|
||||
"cafy": "3.0.0",
|
||||
"chalk": "2.1.0",
|
||||
"compression": "1.7.1",
|
||||
"cors": "2.8.4",
|
||||
"cropperjs": "1.1.3",
|
||||
"crypto": "1.0.1",
|
||||
"debug": "3.1.0",
|
||||
"deep-equal": "1.0.1",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.2",
|
||||
"download": "6.2.5",
|
||||
"elasticsearch": "13.3.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"express": "4.15.4",
|
||||
"file-type": "6.2.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gm": "1.23.0",
|
||||
"inquirer": "3.3.0",
|
||||
"is-root": "1.0.0",
|
||||
"is-url": "1.2.2",
|
||||
"js-yaml": "3.10.0",
|
||||
"mecab-async": "^0.1.0",
|
||||
"moji": "^0.5.1",
|
||||
"mongodb": "2.2.33",
|
||||
"monk": "6.0.5",
|
||||
"morgan": "1.9.0",
|
||||
"ms": "2.0.0",
|
||||
"multer": "1.3.0",
|
||||
"nprogress": "0.2.0",
|
||||
"os-utils": "0.0.14",
|
||||
"page": "1.7.1",
|
||||
"pictograph": "2.0.4",
|
||||
"prominence": "0.2.0",
|
||||
"pug": "2.0.0-rc.4",
|
||||
"ratelimiter": "3.0.3",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "3.2.2",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.83.0",
|
||||
"rimraf": "2.6.2",
|
||||
"riot": "3.7.3",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.0",
|
||||
"serve-favicon": "2.4.5",
|
||||
"summaly": "2.0.3",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"tcp-port-used": "0.1.2",
|
||||
"textarea-caret": "3.0.2",
|
||||
"ts-node": "3.3.0",
|
||||
"typescript": "2.5.3",
|
||||
"uuid": "3.1.0",
|
||||
"vhost": "3.0.2",
|
||||
"websocket": "1.0.25",
|
||||
"xev": "2.0.0"
|
||||
}
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "0.0.3201",
|
||||
"license": "MIT",
|
||||
"description": "A miniblog-based SNS",
|
||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||
"repository": "https://github.com/syuilo/misskey.git",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"config": "node ./tools/init.js",
|
||||
"start": "node ./built",
|
||||
"debug": "DEBUG=misskey:* node ./built",
|
||||
"swagger": "node ./swagger.js",
|
||||
"build": "gulp build",
|
||||
"rebuild": "gulp rebuild",
|
||||
"clean": "gulp clean",
|
||||
"cleanall": "gulp cleanall",
|
||||
"lint": "gulp lint",
|
||||
"test": "gulp test",
|
||||
"format": "gulp format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"@prezzemolo/zip": "0.0.3",
|
||||
"@types/bcryptjs": "2.4.1",
|
||||
"@types/body-parser": "1.16.8",
|
||||
"@types/chai": "4.0.5",
|
||||
"@types/chai-http": "3.0.3",
|
||||
"@types/compression": "0.0.35",
|
||||
"@types/cookie": "0.3.1",
|
||||
"@types/cors": "2.8.3",
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/elasticsearch": "5.0.17",
|
||||
"@types/event-stream": "3.3.33",
|
||||
"@types/eventemitter3": "2.0.2",
|
||||
"@types/express": "4.0.39",
|
||||
"@types/gm": "1.17.33",
|
||||
"@types/gulp": "4.0.3",
|
||||
"@types/gulp-htmlmin": "1.3.31",
|
||||
"@types/gulp-mocha": "0.0.31",
|
||||
"@types/gulp-rename": "0.0.33",
|
||||
"@types/gulp-replace": "0.0.31",
|
||||
"@types/gulp-uglify": "3.0.3",
|
||||
"@types/gulp-util": "3.0.34",
|
||||
"@types/inquirer": "0.0.35",
|
||||
"@types/is-root": "1.0.0",
|
||||
"@types/is-url": "1.2.28",
|
||||
"@types/js-yaml": "3.10.0",
|
||||
"@types/mocha": "2.2.44",
|
||||
"@types/mongodb": "2.2.15",
|
||||
"@types/monk": "1.0.6",
|
||||
"@types/morgan": "1.7.35",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/multer": "1.3.6",
|
||||
"@types/node": "8.0.53",
|
||||
"@types/page": "1.5.32",
|
||||
"@types/proxy-addr": "2.0.0",
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.8.1",
|
||||
"@types/request": "2.0.7",
|
||||
"@types/rimraf": "2.0.2",
|
||||
"@types/riot": "3.6.1",
|
||||
"@types/seedrandom": "2.4.27",
|
||||
"@types/serve-favicon": "2.2.30",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/uuid": "3.4.3",
|
||||
"@types/webpack": "3.8.1",
|
||||
"@types/webpack-stream": "3.2.8",
|
||||
"@types/websocket": "0.0.34",
|
||||
"accesses": "2.5.0",
|
||||
"animejs": "2.2.0",
|
||||
"autwh": "0.0.1",
|
||||
"awesome-typescript-loader": "3.4.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.18.2",
|
||||
"cafy": "3.2.0",
|
||||
"chai": "4.1.2",
|
||||
"chai-http": "3.0.0",
|
||||
"chalk": "2.3.0",
|
||||
"compression": "1.7.1",
|
||||
"cookie": "0.3.1",
|
||||
"cors": "2.8.4",
|
||||
"cropperjs": "1.1.3",
|
||||
"css-loader": "0.28.7",
|
||||
"debug": "3.1.0",
|
||||
"deep-equal": "1.0.1",
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.4",
|
||||
"elasticsearch": "14.0.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"event-stream": "3.3.4",
|
||||
"eventemitter3": "2.0.3",
|
||||
"express": "4.16.2",
|
||||
"file-type": "7.3.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gm": "1.23.0",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cssnano": "2.1.2",
|
||||
"gulp-htmlmin": "3.0.0",
|
||||
"gulp-imagemin": "4.0.0",
|
||||
"gulp-mocha": "4.3.1",
|
||||
"gulp-pug": "3.3.0",
|
||||
"gulp-rename": "1.2.2",
|
||||
"gulp-replace": "0.6.1",
|
||||
"gulp-tslint": "8.1.2",
|
||||
"gulp-typescript": "3.2.3",
|
||||
"gulp-uglify": "3.0.0",
|
||||
"gulp-util": "3.0.8",
|
||||
"inquirer": "4.0.0",
|
||||
"is-root": "1.0.0",
|
||||
"is-url": "1.2.2",
|
||||
"js-yaml": "3.10.0",
|
||||
"mecab-async": "0.1.0",
|
||||
"mocha": "4.0.1",
|
||||
"moji": "0.5.1",
|
||||
"mongodb": "2.2.33",
|
||||
"monk": "6.0.5",
|
||||
"morgan": "1.9.0",
|
||||
"ms": "2.0.0",
|
||||
"multer": "1.3.0",
|
||||
"nprogress": "0.2.0",
|
||||
"os-utils": "0.0.14",
|
||||
"page": "1.7.1",
|
||||
"pictograph": "2.1.2",
|
||||
"prominence": "0.2.0",
|
||||
"proxy-addr": "2.0.2",
|
||||
"pug": "2.0.0-rc.4",
|
||||
"ratelimiter": "3.0.3",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "3.2.2",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.83.0",
|
||||
"rimraf": "2.6.2",
|
||||
"riot": "3.7.4",
|
||||
"riot-tag-loader": "1.0.0",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.0",
|
||||
"seedrandom": "^2.4.3",
|
||||
"serve-favicon": "2.4.5",
|
||||
"sortablejs": "1.7.0",
|
||||
"string-replace-webpack-plugin": "0.1.3",
|
||||
"style-loader": "0.19.0",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.1",
|
||||
"summaly": "2.0.3",
|
||||
"swagger-jsdoc": "1.9.7",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"tcp-port-used": "0.1.2",
|
||||
"textarea-caret": "3.0.2",
|
||||
"tmp": "0.0.33",
|
||||
"ts-node": "3.3.0",
|
||||
"tslint": "5.8.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 getUserSummary from '../../common/get-user-summary';
|
||||
import getNotificationSummary from '../../common/get-notification-summary';
|
||||
|
||||
import Othello, { ai as othelloAi } from '../../common/othello';
|
||||
|
||||
|
@ -62,7 +63,7 @@ export default class BotCore extends EventEmitter {
|
|||
return bot;
|
||||
}
|
||||
|
||||
public async q(query: string): Promise<string | void> {
|
||||
public async q(query: string): Promise<string> {
|
||||
if (this.context != null) {
|
||||
return await this.context.q(query);
|
||||
}
|
||||
|
@ -84,7 +85,10 @@ export default class BotCore extends EventEmitter {
|
|||
'logout, signout: サインアウトします\n' +
|
||||
'post: 投稿します\n' +
|
||||
'tl: タイムラインを見ます\n' +
|
||||
'@<ユーザー名>: ユーザーを表示します';
|
||||
'no: 通知を見ます\n' +
|
||||
'@<ユーザー名>: ユーザーを表示します\n' +
|
||||
'\n' +
|
||||
'タイムラインや通知を見た後、「次」というとさらに遡ることができます。';
|
||||
|
||||
case 'me':
|
||||
return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
|
||||
|
@ -113,7 +117,16 @@ export default class BotCore extends EventEmitter {
|
|||
|
||||
case 'tl':
|
||||
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 '数当てゲーム':
|
||||
|
@ -155,21 +168,7 @@ export default class BotCore extends EventEmitter {
|
|||
this.emit('updated');
|
||||
}
|
||||
|
||||
public async tlCommand(): Promise<string | void> {
|
||||
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> {
|
||||
public async showUserCommand(q: string): Promise<string> {
|
||||
try {
|
||||
const user = await require('../endpoints/users/show')({
|
||||
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 == 'othello') return OthelloContext.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);
|
||||
return null;
|
||||
}
|
||||
|
@ -232,7 +233,7 @@ class SigninContext extends Context {
|
|||
}
|
||||
} else {
|
||||
// Compare password
|
||||
const same = bcrypt.compareSync(query, this.temporaryUser.password);
|
||||
const same = await bcrypt.compare(query, this.temporaryUser.password);
|
||||
|
||||
if (same) {
|
||||
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 {
|
||||
private secret: number;
|
||||
private history: number[] = [];
|
||||
|
|
|
@ -135,6 +135,8 @@ class LineBot extends BotCore {
|
|||
actions: actions
|
||||
}
|
||||
}]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 crypto from 'crypto';
|
||||
import * as gm from 'gm';
|
||||
import * as debug from 'debug';
|
||||
import fileType = require('file-type');
|
||||
import prominence = require('prominence');
|
||||
import DriveFile from '../models/drive-file';
|
||||
|
||||
import DriveFile, { getGridFSBucket } from '../models/drive-file';
|
||||
import DriveFolder from '../models/drive-folder';
|
||||
import serialize from '../serializers/drive-file';
|
||||
import event from '../event';
|
||||
import event, { publishDriveStream } from '../event';
|
||||
import config from '../../conf';
|
||||
|
||||
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
|
||||
*
|
||||
* @param user User who wish to add file
|
||||
* @param fileName File name
|
||||
* @param data Contents
|
||||
* @param file File path or readableStream
|
||||
* @param comment Comment
|
||||
* @param type File type
|
||||
* @param folderId Folder ID
|
||||
* @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
|
||||
*/
|
||||
export default (
|
||||
user: any,
|
||||
data: Buffer,
|
||||
name: string = null,
|
||||
comment: string = null,
|
||||
folderId: mongodb.ObjectID = null,
|
||||
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}`;
|
||||
export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
|
||||
// Get file path
|
||||
new Promise((res: (v: [string, boolean]) => void, rej) => {
|
||||
if (typeof file === 'string') {
|
||||
res([file, false]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (name === null) {
|
||||
name = 'untitled';
|
||||
if (typeof file === 'object' && typeof file.read === 'function') {
|
||||
tmpFile()
|
||||
.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
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(data)
|
||||
.digest('hex') as string;
|
||||
|
||||
log(`hash is ${hash}`);
|
||||
|
||||
if (!force) {
|
||||
// Check if there is a file with the same hash
|
||||
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()
|
||||
// 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
|
||||
event(notifiee, '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);
|
||||
});
|
||||
|
|
52
src/api/common/push-sw.ts
Normal file
52
src/api/common/push-sw.ts
Normal file
|
@ -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 publishUserStream from '../event';
|
||||
import { publishMessagingStream } from '../event';
|
||||
import { publishMessagingIndexStream } from '../event';
|
||||
|
||||
/**
|
||||
* Mark as read message(s)
|
||||
|
@ -49,6 +50,7 @@ export default (
|
|||
|
||||
// Publish event
|
||||
publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
|
||||
publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString()));
|
||||
|
||||
// Calc count of my unread messages
|
||||
const count = await Message
|
||||
|
|
19
src/api/common/signin.ts
Normal file
19
src/api/common/signin.ts
Normal file
|
@ -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: 'sw/register',
|
||||
withCredential: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'i',
|
||||
withCredential: true
|
||||
|
@ -159,6 +164,11 @@ const endpoints: Endpoint[] = [
|
|||
},
|
||||
kind: 'account-write'
|
||||
},
|
||||
{
|
||||
name: 'i/update_home',
|
||||
withCredential: true,
|
||||
kind: 'account-write'
|
||||
},
|
||||
{
|
||||
name: 'i/change_password',
|
||||
withCredential: true
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
|||
if (permissionErr) return rej('invalid permission param');
|
||||
|
||||
// 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().$;
|
||||
if (callbackUrlErr) return rej('invalid callback_url param');
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import $ from 'cafy';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Calculate drive usage
|
||||
const usage = ((await DriveFile
|
||||
.aggregate([
|
||||
{ $match: { user_id: user._id } },
|
||||
{ $match: { 'metadata.user_id': user._id } },
|
||||
{
|
||||
$project: {
|
||||
datasize: true
|
||||
length: true
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$datasize' }
|
||||
usage: { $sum: '$length' }
|
||||
}
|
||||
}
|
||||
]))[0] || {
|
||||
|
|
|
@ -13,35 +13,39 @@ import serialize from '../../serializers/drive-file';
|
|||
* @param {any} app
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user, app) => {
|
||||
// Get 'limit' parameter
|
||||
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
|
||||
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
|
||||
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
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
throw 'cannot set since_id and max_id';
|
||||
}
|
||||
|
||||
// Get 'folder_id' parameter
|
||||
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
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
const query = {
|
||||
user_id: user._id,
|
||||
folder_id: folderId
|
||||
'metadata.user_id': user._id,
|
||||
'metadata.folder_id': folderId
|
||||
} as any;
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
|
@ -53,18 +57,18 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
$lt: maxId
|
||||
};
|
||||
}
|
||||
if (type) {
|
||||
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||
}
|
||||
|
||||
// Issue query
|
||||
const files = await DriveFile
|
||||
.find(query, {
|
||||
fields: {
|
||||
data: false
|
||||
},
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(files.map(async file =>
|
||||
await serialize(file))));
|
||||
});
|
||||
const _files = await Promise.all(files.map(file => serialize(file)));
|
||||
return _files;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import $ from 'cafy';
|
||||
import { validateFileName } from '../../../models/drive-file';
|
||||
import serialize from '../../../serializers/drive-file';
|
||||
|
@ -15,14 +14,11 @@ import create from '../../../common/add-file-to-drive';
|
|||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
||||
module.exports = async (file, params, user): Promise<any> => {
|
||||
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
|
||||
let name = file.originalname;
|
||||
if (name !== undefined && name !== null) {
|
||||
|
@ -32,7 +28,7 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
|||
} else if (name === 'blob') {
|
||||
name = null;
|
||||
} else if (!validateFileName(name)) {
|
||||
return rej('invalid name');
|
||||
throw 'invalid name';
|
||||
}
|
||||
} else {
|
||||
name = null;
|
||||
|
@ -40,14 +36,11 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
|
|||
|
||||
// Get 'folder_id' parameter
|
||||
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
|
||||
const driveFile = await create(user, buffer, name, null, folderId);
|
||||
const driveFile = await create(user, file.path, name, null, folderId);
|
||||
|
||||
// Serialize
|
||||
const fileObj = await serialize(driveFile);
|
||||
|
||||
// Response
|
||||
res(fileObj);
|
||||
});
|
||||
return serialize(driveFile);
|
||||
};
|
||||
|
|
|
@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Issue query
|
||||
const files = await DriveFile
|
||||
.find({
|
||||
name: name,
|
||||
user_id: user._id,
|
||||
folder_id: folderId
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
filename: name,
|
||||
'metadata.user_id': user._id,
|
||||
'metadata.folder_id': folderId
|
||||
});
|
||||
|
||||
// Serialize
|
||||
|
|
|
@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file';
|
|||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user) => {
|
||||
// Get 'file_id' parameter
|
||||
const [fileId, fileIdErr] = $(params.file_id).id().$;
|
||||
if (fileIdErr) return rej('invalid file_id param');
|
||||
if (fileIdErr) throw 'invalid file_id param';
|
||||
|
||||
// Fetch file
|
||||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
return rej('file-not-found');
|
||||
throw 'file-not-found';
|
||||
}
|
||||
|
||||
// Serialize
|
||||
res(await serialize(file, {
|
||||
const _file = await serialize(file, {
|
||||
detail: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return _file;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import DriveFolder from '../../../models/drive-folder';
|
|||
import DriveFile from '../../../models/drive-file';
|
||||
import { validateFileName } from '../../../models/drive-file';
|
||||
import serialize from '../../../serializers/drive-file';
|
||||
import event from '../../../event';
|
||||
import { publishDriveStream } from '../../../event';
|
||||
|
||||
/**
|
||||
* Update a file
|
||||
|
@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
const file = await DriveFile
|
||||
.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (file === null) {
|
||||
|
@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Get 'name' parameter
|
||||
const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
|
||||
if (nameErr) return rej('invalid name param');
|
||||
if (name) file.name = name;
|
||||
if (name) file.filename = name;
|
||||
|
||||
// Get 'folder_id' parameter
|
||||
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 === null) {
|
||||
file.folder_id = null;
|
||||
file.metadata.folder_id = null;
|
||||
} else {
|
||||
// Fetch folder
|
||||
const folder = await DriveFolder
|
||||
|
@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
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: {
|
||||
name: file.name,
|
||||
folder_id: file.folder_id
|
||||
filename: file.filename,
|
||||
'metadata.folder_id': file.metadata.folder_id
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -76,6 +72,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Response
|
||||
res(fileObj);
|
||||
|
||||
// Publish drive_file_updated event
|
||||
event(user._id, 'drive_file_updated', fileObj);
|
||||
// Publish file_updated event
|
||||
publishDriveStream(user._id, 'file_updated', fileObj);
|
||||
});
|
||||
|
|
|
@ -2,11 +2,16 @@
|
|||
* Module dependencies
|
||||
*/
|
||||
import * as URL from 'url';
|
||||
const download = require('download');
|
||||
import $ from 'cafy';
|
||||
import { validateFileName } from '../../../models/drive-file';
|
||||
import serialize from '../../../serializers/drive-file';
|
||||
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
|
||||
|
@ -15,11 +20,11 @@ import create from '../../../common/add-file-to-drive';
|
|||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user): Promise<any> => {
|
||||
// Get 'url' parameter
|
||||
// TODO: Validate this url
|
||||
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();
|
||||
if (!validateFileName(name)) {
|
||||
|
@ -28,17 +33,35 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
|
||||
// Get 'folder_id' parameter
|
||||
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
|
||||
const data = await download(url);
|
||||
// Create temp file
|
||||
const path = await new Promise((res: (string) => void, rej) => {
|
||||
tmp.file((e, path) => {
|
||||
if (e) return rej(e);
|
||||
res(path);
|
||||
});
|
||||
});
|
||||
|
||||
// Create file
|
||||
const driveFile = await create(user, data, name, null, folderId);
|
||||
// write content at URL to temp file
|
||||
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 fileObj = await serialize(driveFile);
|
||||
const driveFile = await create(user, path, name, null, folderId);
|
||||
|
||||
// Response
|
||||
res(fileObj);
|
||||
});
|
||||
// clean-up
|
||||
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 { isValidFolderName } from '../../../models/drive-folder';
|
||||
import serialize from '../../../serializers/drive-folder';
|
||||
import event from '../../../event';
|
||||
import { publishDriveStream } from '../../../event';
|
||||
|
||||
/**
|
||||
* Create drive folder
|
||||
|
@ -52,6 +52,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Response
|
||||
res(folderObj);
|
||||
|
||||
// Publish drive_folder_created event
|
||||
event(user._id, 'drive_folder_created', folderObj);
|
||||
// Publish folder_created event
|
||||
publishDriveStream(user._id, 'folder_created', folderObj);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(folders.map(async folder =>
|
||||
await serialize(folder))));
|
||||
res(await Promise.all(folders.map(folder => serialize(folder))));
|
||||
});
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
import $ from 'cafy';
|
||||
import DriveFolder from '../../../models/drive-folder';
|
||||
import { isValidFolderName } from '../../../models/drive-folder';
|
||||
import serialize from '../../../serializers/drive-file';
|
||||
import event from '../../../event';
|
||||
import serialize from '../../../serializers/drive-folder';
|
||||
import { publishDriveStream } from '../../../event';
|
||||
|
||||
/**
|
||||
* Update a folder
|
||||
|
@ -96,6 +96,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
// Response
|
||||
res(folderObj);
|
||||
|
||||
// Publish drive_folder_updated event
|
||||
event(user._id, 'drive_folder_updated', folderObj);
|
||||
// Publish folder_updated event
|
||||
publishDriveStream(user._id, 'folder_updated', folderObj);
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
_id: -1
|
||||
};
|
||||
const query = {
|
||||
user_id: user._id
|
||||
'metadata.user_id': user._id
|
||||
} as any;
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
|
@ -52,15 +52,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
};
|
||||
}
|
||||
if (type) {
|
||||
query.type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
|
||||
}
|
||||
|
||||
// Issue query
|
||||
const files = await DriveFile
|
||||
.find(query, {
|
||||
fields: {
|
||||
data: false
|
||||
},
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
|
|
@ -13,38 +13,27 @@ import Appdata from '../../../models/appdata';
|
|||
* @param {Boolean} isSecure
|
||||
* @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
|
||||
const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$;
|
||||
if (keyError) return rej('invalid key param');
|
||||
|
||||
if (isSecure) {
|
||||
if (!user.data) {
|
||||
return res();
|
||||
}
|
||||
if (key !== null) {
|
||||
const data = {};
|
||||
data[key] = user.data[key];
|
||||
res(data);
|
||||
} else {
|
||||
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
|
||||
});
|
||||
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) {
|
||||
res(appdata.data);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
if (appdata) {
|
||||
res(appdata.data);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
*/
|
||||
import $ from 'cafy';
|
||||
import Appdata from '../../../models/appdata';
|
||||
import User from '../../../models/user';
|
||||
import serialize from '../../../serializers/user';
|
||||
import event from '../../../event';
|
||||
|
||||
/**
|
||||
* Set app data
|
||||
|
@ -16,7 +13,9 @@ import event from '../../../event';
|
|||
* @param {Boolean} isSecure
|
||||
* @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
|
||||
const [data, dataError] = $(params.data).optional.object()
|
||||
.pipe(obj => {
|
||||
|
@ -43,31 +42,17 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) =
|
|||
set[`data.${key}`] = value;
|
||||
}
|
||||
|
||||
if (isSecure) {
|
||||
const _user = await User.findOneAndUpdate(user._id, {
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
res(204);
|
||||
});
|
||||
|
|
|
@ -22,15 +22,15 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
|||
if (newPasswordErr) return rej('invalid new_password param');
|
||||
|
||||
// Compare password
|
||||
const same = bcrypt.compareSync(currentPassword, user.password);
|
||||
const same = await bcrypt.compare(currentPassword, user.password);
|
||||
|
||||
if (!same) {
|
||||
return rej('incorrect password');
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = bcrypt.genSaltSync(8);
|
||||
const hash = bcrypt.hashSync(newPassword, salt);
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(newPassword, salt);
|
||||
|
||||
await User.update(user._id, {
|
||||
$set: {
|
||||
|
|
|
@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
|
|||
if (passwordErr) return rej('invalid password param');
|
||||
|
||||
// Compare password
|
||||
const same = bcrypt.compareSync(password, user.password);
|
||||
const same = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!same) {
|
||||
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 (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, {
|
||||
$set: {
|
||||
name: user.name,
|
||||
description: user.description,
|
||||
avatar_id: user.avatar_id,
|
||||
banner_id: user.banner_id,
|
||||
profile: user.profile
|
||||
profile: user.profile,
|
||||
'client_settings.show_donation': user.client_settings.show_donation
|
||||
}
|
||||
});
|
||||
|
||||
|
|
60
src/api/endpoints/i/update_home.ts
Normal file
60
src/api/endpoints/i/update_home.ts
Normal file
|
@ -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 serialize from '../../../serializers/messaging-message';
|
||||
import publishUserStream from '../../../event';
|
||||
import { publishMessagingStream } from '../../../event';
|
||||
import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
|
||||
import config from '../../../../conf';
|
||||
|
||||
/**
|
||||
|
@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
if (fileId !== undefined) {
|
||||
file = await DriveFile.findOne({
|
||||
_id: fileId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
data: false
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
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);
|
||||
publishMessagingIndexStream(message.user_id, 'message', messageObj);
|
||||
publishUserStream(message.user_id, 'messaging_message', messageObj);
|
||||
|
||||
// 相手のストリーム
|
||||
publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
|
||||
publishMessagingIndexStream(message.recipient_id, 'message', messageObj);
|
||||
publishUserStream(message.recipient_id, 'messaging_message', messageObj);
|
||||
|
||||
// 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 });
|
||||
if (!freshMessage.is_read) {
|
||||
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||
pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import * as os from 'os';
|
||||
import version from '../../version';
|
||||
import config from '../../conf';
|
||||
import Meta from '../models/meta';
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
@ -39,6 +40,8 @@ import config from '../../conf';
|
|||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params) => new Promise(async (res, rej) => {
|
||||
const meta = (await Meta.findOne()) || {};
|
||||
|
||||
res({
|
||||
maintainer: config.maintainer,
|
||||
version: version,
|
||||
|
@ -49,6 +52,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
|
|||
cpu: {
|
||||
model: os.cpus()[0].model,
|
||||
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 notify from '../../common/notify';
|
||||
import watch from '../../common/watch-post';
|
||||
import { default as event, publishChannelStream } from '../../event';
|
||||
import event, { pushSw, publishChannelStream } from '../../event';
|
||||
import config from '../../../conf';
|
||||
|
||||
/**
|
||||
|
@ -44,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
// SELECT _id
|
||||
const entity = await DriveFile.findOne({
|
||||
_id: mediaId,
|
||||
user_id: user._id
|
||||
}, {
|
||||
_id: true
|
||||
'metadata.user_id': user._id
|
||||
});
|
||||
|
||||
if (entity === null) {
|
||||
|
@ -236,7 +234,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
|
|||
|
||||
const mentions = [];
|
||||
|
||||
function addMention(mentionee, type) {
|
||||
function addMention(mentionee, reason) {
|
||||
// Reject if already added
|
||||
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
|
||||
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 notify from '../../../common/notify';
|
||||
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
|
||||
|
@ -87,6 +89,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
|||
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
|
||||
Watching
|
||||
.find({
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import Post from '../../models/post';
|
||||
import ChannelWatching from '../../models/channel-watching';
|
||||
import getFriends from '../../common/get-friends';
|
||||
|
@ -15,32 +16,41 @@ import serialize from '../../serializers/post';
|
|||
* @param {any} app
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
||||
module.exports = async (params, user, app) => {
|
||||
// Get 'limit' parameter
|
||||
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
|
||||
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
|
||||
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
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
// Get 'since_date' parameter
|
||||
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
|
||||
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 = await getFriends(user._id);
|
||||
|
||||
// Watchしているチャンネルを取得
|
||||
const watches = await ChannelWatching.find({
|
||||
user_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
const { followingIds, watchingChannelIds } = await rap({
|
||||
// ID list of the user itself and other users who the user follows
|
||||
followingIds: getFriends(user._id),
|
||||
// Watchしているチャンネルを取得
|
||||
watchingChannelIds: ChannelWatching.find({
|
||||
user_id: user._id,
|
||||
// 削除されたドキュメントは除く
|
||||
deleted_at: { $exists: false }
|
||||
}).then(watches => watches.map(w => w.channel_id))
|
||||
});
|
||||
|
||||
//#region Construct query
|
||||
|
@ -65,7 +75,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
}, {
|
||||
// Watchしているチャンネルへの投稿
|
||||
channel_id: {
|
||||
$in: watches.map(w => w.channel_id)
|
||||
$in: watchingChannelIds
|
||||
}
|
||||
}]
|
||||
} as any;
|
||||
|
@ -79,6 +89,15 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
query._id = {
|
||||
$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
|
||||
|
||||
|
@ -90,7 +109,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
|
|||
});
|
||||
|
||||
// Serialize
|
||||
res(await Promise.all(timeline.map(async post =>
|
||||
await serialize(post, user)
|
||||
)));
|
||||
});
|
||||
return await Promise.all(timeline.map(post => serialize(post, user)));
|
||||
};
|
||||
|
|
50
src/api/endpoints/sw/register.ts
Normal file
50
src/api/endpoints/sw/register.ts
Normal file
|
@ -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().$;
|
||||
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
|
||||
const user = await User.findOne({
|
||||
_id: userId
|
||||
|
@ -82,8 +86,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
// Sort replies by frequency
|
||||
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
|
||||
|
||||
// Lookup top 10 replies
|
||||
const topRepliedUsers = repliedUsersSorted.slice(0, 10);
|
||||
// Extract top replied users
|
||||
const topRepliedUsers = repliedUsersSorted.slice(0, limit);
|
||||
|
||||
// Make replies object (includes weights)
|
||||
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().$;
|
||||
if (maxIdErr) return rej('invalid max_id param');
|
||||
|
||||
// Check if both of since_id and max_id is specified
|
||||
if (sinceId && maxId) {
|
||||
return rej('cannot set since_id and max_id');
|
||||
// Get 'since_date' parameter
|
||||
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
|
||||
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
|
||||
|
@ -66,13 +74,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
return rej('user not found');
|
||||
}
|
||||
|
||||
// Construct query
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
user_id: user._id
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
|
@ -82,6 +92,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
query._id = {
|
||||
$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) {
|
||||
|
@ -94,6 +113,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
|||
$ne: null
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Issue query
|
||||
const posts = await Post
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import * as redis from 'redis';
|
||||
import swPush from './common/push-sw';
|
||||
import config from '../conf';
|
||||
|
||||
type ID = string | mongo.ObjectID;
|
||||
|
@ -17,6 +18,14 @@ class MisskeyEvent {
|
|||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 const pushSw = ev.publishSw.bind(ev);
|
||||
|
||||
export const publishDriveStream = ev.publishDriveStream.bind(ev);
|
||||
|
||||
export const publishPostStream = ev.publishPostStream.bind(ev);
|
||||
|
||||
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
|
||||
|
||||
export const publishMessagingIndexStream = ev.publishMessagingIndexStream.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');
|
||||
|
||||
(collection as any).createIndex('hash'); // fuck type definition
|
||||
const collection = monkDb.get('drive_files.files');
|
||||
|
||||
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 {
|
||||
return (
|
||||
(name.trim().length > 0) &&
|
||||
|
|
7
src/api/models/meta.ts
Normal file
7
src/api/models/meta.ts
Normal file
|
@ -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 db from '../../db/mongodb';
|
||||
import { IUser } from './user';
|
||||
|
||||
export default db.get('notifications') as any; // fuck type definition
|
||||
|
||||
export interface INotification {
|
||||
_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;
|
||||
}
|
||||
|
|
3
src/api/models/sw-subscription.ts
Normal file
3
src/api/models/sw-subscription.ts
Normal file
|
@ -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 serialize from '../serializers/signin';
|
||||
import event from '../event';
|
||||
import config from '../../conf';
|
||||
import signin from '../common/signin';
|
||||
|
||||
export default async (req: express.Request, res: express.Response) => {
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
|
@ -40,20 +40,10 @@ export default async (req: express.Request, res: express.Response) => {
|
|||
}
|
||||
|
||||
// Compare password
|
||||
const same = bcrypt.compareSync(password, user.password);
|
||||
const same = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (same) {
|
||||
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
|
||||
});
|
||||
|
||||
res.sendStatus(204);
|
||||
signin(res, user, false);
|
||||
} else {
|
||||
res.status(400).send({
|
||||
error: 'incorrect password'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as uuid from 'uuid';
|
||||
import * as express from 'express';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import recaptcha = require('recaptcha-promise');
|
||||
|
@ -8,9 +9,31 @@ import generateUserToken from '../common/generate-native-user-token';
|
|||
import config from '../../conf';
|
||||
|
||||
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) => {
|
||||
// Verify recaptcha
|
||||
// ただしテスト時はこの機構は障害となるため無効にする
|
||||
|
@ -54,12 +77,34 @@ export default async (req: express.Request, res: express.Response) => {
|
|||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = bcrypt.genSaltSync(8);
|
||||
const hash = bcrypt.hashSync(password, salt);
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
// Generate secret
|
||||
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
|
||||
const account: IUser = await User.insert({
|
||||
token: secret,
|
||||
|
@ -88,6 +133,11 @@ export default async (req: express.Request, res: express.Response) => {
|
|||
height: null,
|
||||
location: null,
|
||||
weight: null
|
||||
},
|
||||
settings: {},
|
||||
client_settings: {
|
||||
home: homeData,
|
||||
show_donation: false
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -31,44 +31,44 @@ export default (
|
|||
if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
|
||||
_file = await DriveFile.findOne({
|
||||
_id: file
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (typeof file === 'string') {
|
||||
_file = await DriveFile.findOne({
|
||||
_id: new mongo.ObjectID(file)
|
||||
}, {
|
||||
fields: {
|
||||
data: false
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
_file = deepcopy(file);
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_file.id = _file._id;
|
||||
delete _file._id;
|
||||
if (!_file) return reject('invalid file arg.');
|
||||
|
||||
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
|
||||
_file.folder = await serializeDriveFolder(_file.folder_id, {
|
||||
_target.folder = await serializeDriveFolder(_target.folder_id, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.detail && _file.tags) {
|
||||
if (opts.detail && _target.tags) {
|
||||
// Populate tags
|
||||
_file.tags = await _file.tags.map(async (tag: any) =>
|
||||
_target.tags = await _target.tags.map(async (tag: any) =>
|
||||
await serializeDriveTag(tag)
|
||||
);
|
||||
}
|
||||
|
||||
resolve(_file);
|
||||
resolve(_target);
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ const self = (
|
|||
});
|
||||
|
||||
const childFilesCount = await DriveFile.count({
|
||||
folder_id: _folder.id
|
||||
'metadata.folder_id': _folder.id
|
||||
});
|
||||
|
||||
_folder.folders_count = childFoldersCount;
|
||||
|
|
|
@ -12,6 +12,7 @@ import serializeChannel from './channel';
|
|||
import serializeUser from './user';
|
||||
import serializeDriveFile from './drive-file';
|
||||
import parse from '../common/text';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
||||
/**
|
||||
* Serialize a post
|
||||
|
@ -21,13 +22,13 @@ import parse from '../common/text';
|
|||
* @param options? serialize options
|
||||
* @return response
|
||||
*/
|
||||
const self = (
|
||||
const self = async (
|
||||
post: string | mongo.ObjectID | IPost,
|
||||
me?: string | mongo.ObjectID | IUser,
|
||||
options?: {
|
||||
detail: boolean
|
||||
}
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
) => {
|
||||
const opts = options || {
|
||||
detail: true,
|
||||
};
|
||||
|
@ -56,6 +57,8 @@ const self = (
|
|||
_post = deepcopy(post);
|
||||
}
|
||||
|
||||
if (!_post) throw 'invalid post arg.';
|
||||
|
||||
const id = _post._id;
|
||||
|
||||
// Rename _id to id
|
||||
|
@ -70,105 +73,120 @@ const self = (
|
|||
}
|
||||
|
||||
// Populate user
|
||||
_post.user = await serializeUser(_post.user_id, meId);
|
||||
_post.user = serializeUser(_post.user_id, meId);
|
||||
|
||||
// Populate app
|
||||
if (_post.app_id) {
|
||||
_post.app = await serializeApp(_post.app_id);
|
||||
_post.app = serializeApp(_post.app_id);
|
||||
}
|
||||
|
||||
// Populate channel
|
||||
if (_post.channel_id) {
|
||||
_post.channel = await serializeChannel(_post.channel_id);
|
||||
_post.channel = serializeChannel(_post.channel_id);
|
||||
}
|
||||
|
||||
// Populate media
|
||||
if (_post.media_ids) {
|
||||
_post.media = await Promise.all(_post.media_ids.map(async fileId =>
|
||||
await serializeDriveFile(fileId)
|
||||
_post.media = Promise.all(_post.media_ids.map(fileId =>
|
||||
serializeDriveFile(fileId)
|
||||
));
|
||||
}
|
||||
|
||||
// When requested a detailed post data
|
||||
if (opts.detail) {
|
||||
// Get previous post info
|
||||
const prev = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
$lt: id
|
||||
}
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
},
|
||||
sort: {
|
||||
_id: -1
|
||||
}
|
||||
});
|
||||
_post.prev = prev ? prev._id : null;
|
||||
_post.prev = (async () => {
|
||||
const prev = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
$lt: id
|
||||
}
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
},
|
||||
sort: {
|
||||
_id: -1
|
||||
}
|
||||
});
|
||||
return prev ? prev._id : null;
|
||||
})();
|
||||
|
||||
// Get next post info
|
||||
const next = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
$gt: id
|
||||
}
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
},
|
||||
sort: {
|
||||
_id: 1
|
||||
}
|
||||
});
|
||||
_post.next = next ? next._id : null;
|
||||
_post.next = (async () => {
|
||||
const next = await Post.findOne({
|
||||
user_id: _post.user_id,
|
||||
_id: {
|
||||
$gt: id
|
||||
}
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
},
|
||||
sort: {
|
||||
_id: 1
|
||||
}
|
||||
});
|
||||
return next ? next._id : null;
|
||||
})();
|
||||
|
||||
if (_post.reply_id) {
|
||||
// Populate reply to post
|
||||
_post.reply = await self(_post.reply_id, meId, {
|
||||
_post.reply = self(_post.reply_id, meId, {
|
||||
detail: false
|
||||
});
|
||||
}
|
||||
|
||||
if (_post.repost_id) {
|
||||
// Populate repost
|
||||
_post.repost = await self(_post.repost_id, meId, {
|
||||
_post.repost = self(_post.repost_id, meId, {
|
||||
detail: _post.text == null
|
||||
});
|
||||
}
|
||||
|
||||
// Poll
|
||||
if (meId && _post.poll) {
|
||||
const vote = await Vote
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
post_id: id
|
||||
});
|
||||
_post.poll = (async (poll) => {
|
||||
const vote = await Vote
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
post_id: id
|
||||
});
|
||||
|
||||
if (vote != null) {
|
||||
const myChoice = _post.poll.choices
|
||||
.filter(c => c.id == vote.choice)[0];
|
||||
if (vote != null) {
|
||||
const myChoice = poll.choices
|
||||
.filter(c => c.id == vote.choice)[0];
|
||||
|
||||
myChoice.is_voted = true;
|
||||
}
|
||||
myChoice.is_voted = true;
|
||||
}
|
||||
|
||||
return poll;
|
||||
})(_post.poll);
|
||||
}
|
||||
|
||||
// Fetch my reaction
|
||||
if (meId) {
|
||||
const reaction = await Reaction
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
post_id: id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_post.my_reaction = (async () => {
|
||||
const reaction = await Reaction
|
||||
.findOne({
|
||||
user_id: meId,
|
||||
post_id: id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
|
||||
if (reaction) {
|
||||
_post.my_reaction = reaction.reaction;
|
||||
}
|
||||
if (reaction) {
|
||||
return reaction.reaction;
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
resolve(_post);
|
||||
});
|
||||
// resolve promises in _post object
|
||||
_post = await rap(_post);
|
||||
|
||||
return _post;
|
||||
};
|
||||
|
||||
export default self;
|
||||
|
|
|
@ -8,6 +8,7 @@ import serializePost from './post';
|
|||
import Following from '../models/following';
|
||||
import getFriends from '../common/get-friends';
|
||||
import config from '../../conf';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
||||
/**
|
||||
* Serialize a user
|
||||
|
@ -34,9 +35,10 @@ export default (
|
|||
let _user: any;
|
||||
|
||||
const fields = opts.detail ? {
|
||||
data: false
|
||||
settings: false
|
||||
} : {
|
||||
data: false,
|
||||
settings: false,
|
||||
client_settings: false,
|
||||
profile: false,
|
||||
keywords: false,
|
||||
domains: false
|
||||
|
@ -55,6 +57,8 @@ export default (
|
|||
_user = deepcopy(user);
|
||||
}
|
||||
|
||||
if (!_user) return reject('invalid user arg.');
|
||||
|
||||
// Me
|
||||
const meId: mongo.ObjectID = me
|
||||
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
||||
|
@ -69,7 +73,7 @@ export default (
|
|||
delete _user._id;
|
||||
|
||||
// Remove needless properties
|
||||
delete _user.lates_post;
|
||||
delete _user.latest_post;
|
||||
|
||||
// Remove private properties
|
||||
delete _user.password;
|
||||
|
@ -83,8 +87,8 @@ export default (
|
|||
|
||||
// Visible via only the official client
|
||||
if (!opts.includeSecrets) {
|
||||
delete _user.data;
|
||||
delete _user.email;
|
||||
delete _user.client_settings;
|
||||
}
|
||||
|
||||
_user.avatar_url = _user.avatar_id != null
|
||||
|
@ -104,26 +108,30 @@ export default (
|
|||
|
||||
if (meId && !meId.equals(_user.id)) {
|
||||
// If the user is following
|
||||
const follow = await Following.findOne({
|
||||
follower_id: meId,
|
||||
followee_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.is_following = follow !== null;
|
||||
_user.is_following = (async () => {
|
||||
const follow = await Following.findOne({
|
||||
follower_id: meId,
|
||||
followee_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
return follow !== null;
|
||||
})();
|
||||
|
||||
// If the user is followed
|
||||
const follow2 = await Following.findOne({
|
||||
follower_id: _user.id,
|
||||
followee_id: meId,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.is_followed = follow2 !== null;
|
||||
_user.is_followed = (async () => {
|
||||
const follow2 = await Following.findOne({
|
||||
follower_id: _user.id,
|
||||
followee_id: meId,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
return follow2 !== null;
|
||||
})();
|
||||
}
|
||||
|
||||
if (opts.detail) {
|
||||
if (_user.pinned_post_id) {
|
||||
// Populate pinned post
|
||||
_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
|
||||
_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
|
@ -132,23 +140,24 @@ export default (
|
|||
const myFollowingIds = await getFriends(meId);
|
||||
|
||||
// Get following you know count
|
||||
const followingYouKnowCount = await Following.count({
|
||||
_user.following_you_know_count = Following.count({
|
||||
followee_id: { $in: myFollowingIds },
|
||||
follower_id: _user.id,
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.following_you_know_count = followingYouKnowCount;
|
||||
|
||||
// Get followers you know count
|
||||
const followersYouKnowCount = await Following.count({
|
||||
_user.followers_you_know_count = Following.count({
|
||||
followee_id: _user.id,
|
||||
follower_id: { $in: myFollowingIds },
|
||||
deleted_at: { $exists: false }
|
||||
});
|
||||
_user.followers_you_know_count = followersYouKnowCount;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve promises in _user object
|
||||
_user = await rap(_user);
|
||||
|
||||
resolve(_user);
|
||||
});
|
||||
/*
|
||||
|
|
|
@ -40,7 +40,7 @@ app.get('/', (req, res) => {
|
|||
endpoints.forEach(endpoint =>
|
||||
endpoint.withFile ?
|
||||
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)) :
|
||||
app.post(`/${endpoint.name}`,
|
||||
require('./api-handler').default.bind(null, endpoint))
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as express from 'express';
|
||||
import * as cookie from 'cookie';
|
||||
import * as uuid from 'uuid';
|
||||
// import * as Twitter from 'twitter';
|
||||
// const Twitter = require('twitter');
|
||||
import autwh from 'autwh';
|
||||
|
@ -7,6 +9,7 @@ import User from '../models/user';
|
|||
import serialize from '../serializers/user';
|
||||
import event from '../event';
|
||||
import config from '../../conf';
|
||||
import signin from '../common/signin';
|
||||
|
||||
module.exports = (app: express.Application) => {
|
||||
app.get('/disconnect/twitter', async (req, res): Promise<any> => {
|
||||
|
@ -30,8 +33,13 @@ module.exports = (app: express.Application) => {
|
|||
|
||||
if (config.twitter == null) {
|
||||
app.get('/connect/twitter', (req, res) => {
|
||||
res.send('現在Twitterへ接続できません');
|
||||
res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
|
||||
});
|
||||
|
||||
app.get('/signin/twitter', (req, res) => {
|
||||
res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -48,14 +56,58 @@ module.exports = (app: express.Application) => {
|
|||
res.redirect(ctx.url);
|
||||
});
|
||||
|
||||
app.get('/tw/cb', (req, res): any => {
|
||||
if (res.locals.user == null) return res.send('plz signin');
|
||||
redis.get(res.locals.user, async (_, ctx) => {
|
||||
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
|
||||
app.get('/signin/twitter', async (req, res): Promise<any> => {
|
||||
const ctx = await twAuth.begin();
|
||||
|
||||
const user = await User.findOneAndUpdate({
|
||||
token: res.locals.user
|
||||
}, {
|
||||
const sessid = uuid();
|
||||
|
||||
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: {
|
||||
twitter: {
|
||||
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
|
||||
event(user._id, 'i_updated', await serialize(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
}));
|
||||
});
|
||||
// Publish i updated event
|
||||
event(user._id, 'i_updated', await serialize(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
10
src/api/stream/drive.ts
Normal file
10
src/api/stream/drive.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
10
src/api/stream/messaging-index.ts
Normal file
10
src/api/stream/messaging-index.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
19
src/api/stream/requests.ts
Normal file
19
src/api/stream/requests.ts
Normal file
|
@ -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 homeStream from './stream/home';
|
||||
import driveStream from './stream/drive';
|
||||
import messagingStream from './stream/messaging';
|
||||
import messagingIndexStream from './stream/messaging-index';
|
||||
import serverStream from './stream/server';
|
||||
import requestsStream from './stream/requests';
|
||||
import channelStream from './stream/channel';
|
||||
|
||||
module.exports = (server: http.Server) => {
|
||||
|
@ -27,6 +30,11 @@ module.exports = (server: http.Server) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (request.resourceURL.pathname === '/requests') {
|
||||
requestsStream(request, connection);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Redis
|
||||
const subscriber = redis.createClient(
|
||||
config.redis.port, config.redis.host);
|
||||
|
@ -51,7 +59,9 @@ module.exports = (server: http.Server) => {
|
|||
|
||||
const channel =
|
||||
request.resourceURL.pathname === '/' ? homeStream :
|
||||
request.resourceURL.pathname === '/drive' ? driveStream :
|
||||
request.resourceURL.pathname === '/messaging' ? messagingStream :
|
||||
request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
|
||||
null;
|
||||
|
||||
if (channel !== null) {
|
||||
|
|
27
src/common/get-notification-summary.ts
Normal file
27
src/common/get-notification-summary.ts
Normal file
|
@ -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}>`;
|
||||
}
|
||||
}
|
14
src/common/get-reaction-emoji.ts
Normal file
14
src/common/get-reaction-emoji.ts
Normal file
|
@ -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 URL from 'url';
|
||||
import * as yaml from 'js-yaml';
|
||||
import isUrl = require('is-url');
|
||||
|
||||
|
@ -23,16 +22,23 @@ export const path = process.env.NODE_ENV == 'test'
|
|||
* ユーザーが設定する必要のある情報
|
||||
*/
|
||||
type Source = {
|
||||
maintainer: string;
|
||||
/**
|
||||
* メンテナ情報
|
||||
*/
|
||||
maintainer: {
|
||||
/**
|
||||
* メンテナの名前
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* メンテナの連絡先(URLかmailto形式のURL)
|
||||
*/
|
||||
url: string;
|
||||
};
|
||||
url: string;
|
||||
secondary_url: string;
|
||||
port: number;
|
||||
https: {
|
||||
enable: boolean;
|
||||
key: string;
|
||||
cert: string;
|
||||
ca: string;
|
||||
};
|
||||
https?: { [x: string]: string };
|
||||
mongodb: {
|
||||
host: string;
|
||||
port: number;
|
||||
|
@ -52,8 +58,8 @@ type Source = {
|
|||
pass: string;
|
||||
};
|
||||
recaptcha: {
|
||||
siteKey: string;
|
||||
secretKey: string;
|
||||
site_key: string;
|
||||
secret_key: string;
|
||||
};
|
||||
accesslog?: string;
|
||||
accesses?: {
|
||||
|
@ -75,6 +81,14 @@ type Source = {
|
|||
analysis?: {
|
||||
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.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.secondary_url = normalizeUrl(config.secondary_url);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"themeColor": "#f43636",
|
||||
"themeColor": "#ff4e45",
|
||||
"themeColorForeground": "#fff"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,38 @@
|
|||
import * as mongo from 'monk';
|
||||
|
||||
import config from '../conf';
|
||||
|
||||
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.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}`;
|
||||
|
||||
/**
|
||||
* monk
|
||||
*/
|
||||
import * as mongo from 'monk';
|
||||
|
||||
const db = mongo(uri);
|
||||
|
||||
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 };
|
||||
|
|
BIN
src/file/assets/not-an-image.png
Normal file
BIN
src/file/assets/not-an-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
src/file/assets/thumbnail-not-available.png
Normal file
BIN
src/file/assets/thumbnail-not-available.png
Normal file
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 mongodb from 'mongodb';
|
||||
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
|
||||
|
@ -33,101 +34,127 @@ app.get('/', (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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
async function raw(data: Buffer, type: string, download: boolean, res: express.Response): Promise<any> {
|
||||
res.header('Content-Type', type);
|
||||
|
||||
if (download) {
|
||||
res.header('Content-Disposition', 'attachment');
|
||||
}
|
||||
|
||||
res.send(data);
|
||||
interface ISend {
|
||||
contentType: string;
|
||||
stream: stream.Readable;
|
||||
}
|
||||
|
||||
async function thumbnail(data: Buffer, type: string, resize: number, res: express.Response): Promise<any> {
|
||||
if (!/^image\/.*$/.test(type)) {
|
||||
data = fs.readFileSync(`${__dirname}/assets/dummy.png`);
|
||||
}
|
||||
function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
|
||||
const readable: stream.Readable = (() => {
|
||||
// 画像ではない場合
|
||||
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) {
|
||||
g = g.resize(resize, resize);
|
||||
}
|
||||
|
||||
g
|
||||
const stream = g
|
||||
.compress('jpeg')
|
||||
.quality(80)
|
||||
.toBuffer('jpeg', (err, img) => {
|
||||
if (err !== undefined && err !== null) {
|
||||
console.error(err);
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
.stream();
|
||||
|
||||
res.header('Content-Type', 'image/jpeg');
|
||||
res.send(img);
|
||||
});
|
||||
return {
|
||||
contentType: 'image/jpeg',
|
||||
stream
|
||||
};
|
||||
}
|
||||
|
||||
function send(data: Buffer, type: string, req: express.Request, res: express.Response): void {
|
||||
if (req.query.thumbnail !== undefined) {
|
||||
thumbnail(data, type, req.query.size, res);
|
||||
} else {
|
||||
raw(data, type, req.query.download !== undefined, res);
|
||||
const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => {
|
||||
console.dir(e);
|
||||
req.destroy();
|
||||
res.destroy(e);
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
app.get('/:id', 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
app.get('/:id', sendFileById);
|
||||
app.get('/:id/:name', sendFileById);
|
||||
|
||||
module.exports = app;
|
||||
|
|
|
@ -8,7 +8,7 @@ import * as fs from 'fs';
|
|||
import * as os from 'os';
|
||||
import * as cluster from 'cluster';
|
||||
import * as debug from 'debug';
|
||||
import * as chalk from 'chalk';
|
||||
import chalk from 'chalk';
|
||||
// import portUsed = require('tcp-port-used');
|
||||
import isRoot = require('is-root');
|
||||
import { master } from 'accesses';
|
||||
|
|
21
src/log-request.ts
Normal file
21
src/log-request.ts
Normal file
|
@ -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 vhost = require('vhost');
|
||||
|
||||
import log from './log-request';
|
||||
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
|
||||
}));
|
||||
|
||||
// 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) => {
|
||||
if (!req.headers['host']) {
|
||||
res.sendStatus(400);
|
||||
|
@ -55,13 +61,17 @@ app.use(require('./web/server'));
|
|||
/**
|
||||
* Create server
|
||||
*/
|
||||
const server = config.https.enable ?
|
||||
https.createServer({
|
||||
key: fs.readFileSync(config.https.key),
|
||||
cert: fs.readFileSync(config.https.cert),
|
||||
ca: fs.readFileSync(config.https.ca)
|
||||
}, app) :
|
||||
http.createServer(app);
|
||||
const server = (() => {
|
||||
if (config.https) {
|
||||
const certs = {};
|
||||
Object.keys(config.https).forEach(k => {
|
||||
certs[k] = fs.readFileSync(config.https[k]);
|
||||
});
|
||||
return https.createServer(certs, app);
|
||||
} else {
|
||||
return http.createServer(app);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Steaming
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import * as readline from 'readline';
|
||||
import * as chalk from 'chalk';
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Progress bar
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as chalk from 'chalk';
|
||||
import chalk, { Chalk } from 'chalk';
|
||||
|
||||
export type LogLevel = 'Error' | 'Warn' | 'Info';
|
||||
|
||||
function toLevelColor(level: LogLevel): chalk.ChalkStyle {
|
||||
function toLevelColor(level: LogLevel): Chalk {
|
||||
switch (level) {
|
||||
case 'Error': return chalk.red;
|
||||
case 'Warn': return chalk.yellow;
|
||||
|
|
|
@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
|
|||
/**
|
||||
* init
|
||||
*/
|
||||
init(me => {
|
||||
init(() => {
|
||||
mount(document.createElement('mk-index'));
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ html
|
|||
meta(name='application-name' content='Misskey')
|
||||
meta(name='theme-color' content=themeColor)
|
||||
meta(name='referrer' content='origin')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
|
||||
title Misskey
|
||||
|
||||
|
|
|
@ -27,7 +27,9 @@
|
|||
// misskey.alice => misskey
|
||||
// misskey.strawberry.pasta => misskey
|
||||
// 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
|
||||
// Note: The default language is English
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as riot from 'riot';
|
||||
const route = require('page');
|
||||
import * as route from 'page';
|
||||
let page = null;
|
||||
|
||||
export default me => {
|
||||
export default () => {
|
||||
route('/', index);
|
||||
route('/:channel', channel);
|
||||
route('*', notFound);
|
||||
|
@ -22,7 +22,7 @@ export default me => {
|
|||
}
|
||||
|
||||
// EXEC
|
||||
route();
|
||||
(route as any)();
|
||||
};
|
||||
|
||||
function mount(content) {
|
|
@ -12,7 +12,7 @@ import route from './router';
|
|||
/**
|
||||
* init
|
||||
*/
|
||||
init(me => {
|
||||
init(() => {
|
||||
// Start routing
|
||||
route(me);
|
||||
route();
|
||||
});
|
|
@ -26,11 +26,11 @@
|
|||
<hr>
|
||||
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
||||
<div if={ !SIGNIN }>
|
||||
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
|
||||
<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
|
||||
</div>
|
||||
<hr>
|
||||
<footer>
|
||||
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
|
||||
<small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
|
||||
</footer>
|
||||
</main>
|
||||
<style>
|
||||
|
@ -55,7 +55,7 @@
|
|||
</style>
|
||||
<script>
|
||||
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('api');
|
||||
|
@ -66,7 +66,6 @@
|
|||
this.channel = null;
|
||||
this.posts = null;
|
||||
this.connection = new ChannelStream(this.id);
|
||||
this.version = VERSION;
|
||||
this.unreadCount = 0;
|
||||
|
||||
this.on('mount', () => {
|
||||
|
@ -166,7 +165,7 @@
|
|||
<mk-channel-post>
|
||||
<header>
|
||||
<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 } mode="detail"/>
|
||||
<span>ID:<i>{ post.user.username }</i></span>
|
||||
|
@ -284,8 +283,6 @@
|
|||
|
||||
</style>
|
||||
<script>
|
||||
import CONFIG from '../../common/scripts/config';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.channel = this.opts.channel;
|
||||
|
@ -343,7 +340,7 @@
|
|||
};
|
||||
|
||||
this.changeFile = () => {
|
||||
this.refs.file.files.forEach(this.upload);
|
||||
Array.from(this.refs.file.files).forEach(this.upload);
|
||||
};
|
||||
|
||||
this.selectFile = () => {
|
||||
|
@ -357,7 +354,7 @@
|
|||
});
|
||||
};
|
||||
|
||||
window.open(CONFIG.url + '/selectdrive?multiple=true',
|
||||
window.open(_URL_ + '/selectdrive?multiple=true',
|
||||
'drive_window',
|
||||
'height=500,width=800');
|
||||
};
|
||||
|
@ -367,7 +364,7 @@
|
|||
};
|
||||
|
||||
this.onpaste = e => {
|
||||
e.clipboardData.items.forEach(item => {
|
||||
Array.from(e.clipboardData.items).forEach(item => {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
}
|
||||
|
@ -390,7 +387,7 @@
|
|||
</mk-twitter-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>
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<mk-header>
|
||||
<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>
|
||||
<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
|
||||
<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
|
||||
<a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
|
||||
<a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('channels').then(channels => {
|
||||
this.api('channels', {
|
||||
limit: 100
|
||||
}).then(channels => {
|
||||
this.update({
|
||||
channels: channels
|
||||
});
|
||||
|
|
351
src/web/app/common/mios.ts
Normal file
351
src/web/app/common/mios.ts
Normal file
|
@ -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;
|
||||
}
|
40
src/web/app/common/mixins.ts
Normal file
40
src/web/app/common/mixins.ts
Normal file
|
@ -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
|
||||
*/
|
||||
|
||||
import CONFIG from './config';
|
||||
declare const _API_URL_: string;
|
||||
|
||||
let spinner = null;
|
||||
let pending = 0;
|
||||
|
@ -14,7 +14,7 @@ let pending = 0;
|
|||
* @param {any} [data={}] Data
|
||||
* @return {Promise<any>} Response
|
||||
*/
|
||||
export default (i, endpoint, data = {}) => {
|
||||
export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
|
||||
if (++pending === 1) {
|
||||
spinner = document.createElement('div');
|
||||
spinner.setAttribute('id', 'wait');
|
||||
|
@ -22,11 +22,11 @@ export default (i, endpoint, data = {}) => {
|
|||
}
|
||||
|
||||
// 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) => {
|
||||
// Send request
|
||||
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${CONFIG.apiUrl}/${endpoint}`, {
|
||||
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${_API_URL_}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: endpoint === 'signin' ? 'include' : 'omit'
|
|
@ -1,6 +1,6 @@
|
|||
export default (bytes, digits = 0) => {
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
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];
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
12
src/web/app/common/scripts/check-for-update.ts
Normal file
12
src/web/app/common/scripts/check-for-update.ts
Normal file
|
@ -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_));
|
||||
}
|
||||
}
|
60
src/web/app/common/scripts/compose-notification.ts
Normal file
60
src/web/app/common/scripts/compose-notification.ts
Normal file
|
@ -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 a new issue