Merge branch 'master' of github.com:syuilo/misskey

This commit is contained in:
ha-dai 2017-11-27 03:41:47 +09:00
commit 5edc0227e8
275 changed files with 7445 additions and 2804 deletions

1
.gitignore vendored
View File

@ -2,7 +2,6 @@
/.vscode
/node_modules
/built
/uploads
/data
npm-debug.log
*.pem

View File

@ -22,5 +22,5 @@ elasticsearch:
port: 9200
pass: ''
recaptcha:
siteKey: hima
secretKey: saku
site_key: hima
secret_key: saku

View File

@ -22,5 +22,5 @@ elasticsearch:
port: 9200
pass: ''
recaptcha:
siteKey: hima
secretKey: saku
site_key: hima
secret_key: saku

View File

@ -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)
-----------------
* いい感じに

View File

@ -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].

View File

@ -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

View File

@ -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
View 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:
```

View File

@ -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`

View File

@ -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`

View File

@ -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
);

View File

@ -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"

View File

@ -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: "新規登録"

View File

@ -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"
}
}

View File

@ -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[] = [];

View File

@ -135,6 +135,8 @@ class LineBot extends BotCore {
actions: actions
}
}]);
return null;
}
public async showUserTimelinePostback(userId: string) {

View File

@ -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);
});

View File

@ -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
View 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
});
}
});
});
}

View File

@ -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
View 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);
}
}

View File

@ -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

View File

@ -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');

View File

@ -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';
/**

View File

@ -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] || {

View File

@ -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;
};

View File

@ -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);
};

View File

@ -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

View File

@ -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;
};

View 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);
});

View File

@ -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);
};

View File

@ -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);
});

View File

@ -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))));
});

View File

@ -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);
});

View File

@ -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
});

View File

@ -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();
}
});

View File

@ -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);
});

View File

@ -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: {

View File

@ -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');

View File

@ -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
}
});

View 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();
}
});

View File

@ -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);

View File

@ -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
});
});

View File

@ -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);
}
}

View File

@ -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({

View File

@ -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)));
};

View 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();
});

View File

@ -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) => ({

View File

@ -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

View File

@ -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);

View File

@ -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
View 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;
};

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
import db from '../../db/mongodb';
export default db.get('sw_subscriptions') as any; // fuck type definition

View File

@ -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'

View File

@ -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
}
});

View File

@ -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);
});

View File

@ -44,7 +44,7 @@ const self = (
});
const childFilesCount = await DriveFile.count({
folder_id: _folder.id
'metadata.folder_id': _folder.id
});
_folder.folders_count = childFoldersCount;

View File

@ -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;

View File

@ -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);
});
/*

View File

@ -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))

View File

@ -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
View 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);
});
}

View 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);
});
}

View 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);
});
}

View File

@ -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) {

View 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}>`;
}
}

View 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 '';
}
}

View File

@ -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);

View File

@ -1,4 +1,4 @@
{
"themeColor": "#f43636",
"themeColor": "#ff4e45",
"themeColorForeground": "#fff"
}

View File

@ -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 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
src/file/assets/thumbnail-not-available.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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;

View File

@ -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
View 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
});
}

View File

@ -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

View File

@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import * as readline from 'readline';
import * as chalk from 'chalk';
import chalk from 'chalk';
/**
* Progress bar

View File

@ -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;

View File

@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
/**
* init
*/
init(me => {
init(() => {
mount(document.createElement('mk-index'));
});

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -12,7 +12,7 @@ import route from './router';
/**
* init
*/
init(me => {
init(() => {
// Start routing
route(me);
route();
});

View File

@ -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];

View File

@ -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

View File

@ -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
View 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;
}

View 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) });
};

View File

@ -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)
});
};

View File

@ -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
});
};

View File

@ -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);
};

View File

@ -1,5 +0,0 @@
import * as riot from 'riot';
export default stream => {
riot.mixin('stream', { stream });
};

View File

@ -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'

View File

@ -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];
};

View File

@ -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));
}
});
});
};

View 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_));
}
}

View 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