1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-12-25 12:08:16 +09:00

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

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -2,6 +2,219 @@ ChangeLog (Release Notes)
========================= =========================
主に notable な changes を書いていきます 主に notable な changes を書いていきます
3201 (2017/11/23)
-----------------
* Twitterログインを実装 (#939)
3196 (2017/11/23)
-----------------
* バグ修正
3194 (2017/11/23)
-----------------
* バグ修正
3191 (2017/11/23)
-----------------
* :v:
3188 (2017/11/22)
-----------------
* バグ修正
3180 (2017/11/21)
-----------------
* バグ修正
3177 (2017/11/21)
-----------------
* ServiceWorker support
* Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ)
3165 (2017/11/20)
-----------------
* デスクトップ版でも通知バッジを表示 (#918)
* デザインの調整
* バグ修正
3155 (2017/11/20)
-----------------
* デスクトップ版でユーザーの投稿グラフを見れるように
3142 (2017/11/18)
-----------------
* バグ修正
3140 (2017/11/18)
-----------------
* ウィジェットをスクロールに追従させるように
3136 (2017/11/17)
-----------------
* バグ修正
* 通信の最適化
3131 (2017/11/17)
-----------------
* バグ修正
* 通信の最適化
3124 (2017/11/16)
-----------------
* バグ修正
3121 (2017/11/16)
-----------------
* ブロードキャストウィジェットの強化
* デザインのグリッチの修正
* 通信の最適化
3113 (2017/11/15)
-----------------
* アクティビティのレンダリングの問題の修正など
3110 (2017/11/15)
-----------------
* デザインの調整など
3107 (2017/11/14)
-----------------
* デザインの調整
3104 (2017/11/14)
-----------------
* デスクトップ版ユーザーページのデザインの改良
* バグ修正
3099 (2017/11/14)
-----------------
* デスクトップ版ユーザーページの強化
* バグ修正
3093 (2017/11/14)
-----------------
* やった
3089 (2017/11/14)
-----------------
* なんか
3069 (2017/11/14)
-----------------
* ドライブウィンドウもポップアウトできるように
* デザインの調整
3066 (2017/11/14)
-----------------
* メッセージウィジェット追加
* アクセスログウィジェット追加
3057 (2017/11/13)
-----------------
* グリッチ修正
3055 (2017/11/13)
-----------------
* メッセージのウィンドウのポップアウト (#911)
3050 (2017/11/13)
-----------------
* 通信の最適化
* これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる
* デザインの調整
* ユーザビリティの向上
3040 (2017/11/12)
-----------------
* バグ修正
3038 (2017/11/12)
-----------------
* 投稿フォームウィジェットの追加
* タイムライン上部にもウィジェットを配置できるように
3035 (2017/11/12)
-----------------
* ウィジェットの強化
3033 (2017/11/12)
-----------------
* デザインの調整
3031 (2017/11/12)
-----------------
* ウィジェットの強化
3028 (2017/11/12)
-----------------
* ウィジェットの表示をコンパクトにできるように
3026 (2017/11/12)
-----------------
* バグ修正
3024 (2017/11/12)
-----------------
* いい感じにするなど
3020 (2017/11/12)
-----------------
* 通信の最適化
3017 (2017/11/11)
-----------------
* 誤字修正など
3012 (2017/11/11)
-----------------
* デザインの調整
3010 (2017/11/11)
-----------------
* デザインの調整
3008 (2017/11/11)
-----------------
* カレンダー(タイムマシン)ウィジェットの追加
3006 (2017/11/11)
-----------------
* デザインの調整
* など
2996 (2017/11/10)
-----------------
* デザインの調整
* など
2991 (2017/11/09)
-----------------
* デザインの調整
2988 (2017/11/09)
-----------------
* チャンネルウィジェットを追加
2984 (2017/11/09)
-----------------
* スライドショーウィジェットを追加
2974 (2017/11/08)
-----------------
* ホームのカスタマイズを実装するなど
2971 (2017/11/08)
-----------------
* バグ修正
* デザインの調整
* i18n
2944 (2017/11/07)
-----------------
* パフォーマンスの向上
* GirdFSになるなどした
* 依存関係の更新
2807 (2017/11/02) 2807 (2017/11/02)
----------------- -----------------
* いい感じに * いい感じに

View File

@ -1,5 +1,6 @@
DONORS DONORS
====== ======
The list of people who have sent donation for Misskey.
(no particular order) (no particular order)
@ -7,12 +8,14 @@ DONORS
* 俺様 * 俺様
* なぎうり * なぎうり
* スルメ https://surume.tk/ * スルメ https://surume.tk/
* 藍
* 音船 https://otofune.me/
:heart: Thanks for donating, guys! :heart: Thanks for donating, guys!
--- ---
Although you donated, you are not listed here? please contact to us! If your name is missing, please contact us!
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].

View File

@ -17,7 +17,7 @@ Key features
* Automatically updated timeline * Automatically updated timeline
* Private messages * Private messages
* Free 1GB storage for each all users * Free 1GB storage for each all users
* Machine learning * ServiceWorker support
* Web API for third-party applications * Web API for third-party applications
* No ads * No ads
@ -38,10 +38,18 @@ Please see [ChangeLog](./CHANGELOG.md).
Sponsors & Backers Sponsors & Backers
---------------------------------------------------------------- ----------------------------------------------------------------
Misskey have no 100+ GitHub stars currently. However, donation are always welcome! Misskey has no 100+ GitHub stars currently. However, a donation is always welcome!
If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md). **Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md).
Collaborators
----------------------------------------------------------------
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] |
|------------------------|-----------------------------------|---------------------------------|
| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] |
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
Copyright Copyright
---------------------------------------------------------------- ----------------------------------------------------------------
@ -51,8 +59,8 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
[mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square [mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square
[travis-link]: https://travis-ci.org/syuilo/misskey [travis-link]: https://travis-ci.org/syuilo/misskey
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square [travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
[dependencies-link]: https://gemnasium.com/syuilo/misskey [dependencies-link]: https://david-dm.org/syuilo/misskey
[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
[himasaku]: https://himasaku.net [himasaku]: https://himasaku.net
[himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square [himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
[sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square [sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
@ -60,3 +68,7 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
<!-- Collaborators Info --> <!-- Collaborators Info -->
[syuilo-link]: https://syuilo.com [syuilo-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
[ayamorisawa-link]: https://github.com/ayamorisawa
[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
[otofune-link]: https://github.com/otofune
[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70

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 Misskey Setup and Installation Guide
================================================================ ================================================================
We thank you for your interest in setup your Misskey server! We thank you for your interest in setting up your Misskey server!
This guide describes how to install and setup Misskey. This guide describes how to install and setup Misskey.
[Japanese version also available - 日本語版もあります](./setup.ja.md) [Japanese version also available - 日本語版もあります](./setup.ja.md)
@ -36,6 +36,15 @@ Note that Misskey uses following subdomains:
Misskey requires reCAPTCHA tokens. Misskey requires reCAPTCHA tokens.
Please visit https://www.google.com/recaptcha/intro/ and generate keys. Please visit https://www.google.com/recaptcha/intro/ and generate keys.
*(optional)* Generating VAPID keys
----------------------------------------------------------------
If you want to enable ServiceWroker, you need to generate VAPID keys:
``` shell
npm install web-push -g
web-push generate-vapid-keys
```
*3.* Install dependencies *3.* Install dependencies
---------------------------------------------------------------- ----------------------------------------------------------------
Please install and setup these softwares: Please install and setup these softwares:
@ -51,24 +60,6 @@ Please install and setup these softwares:
*4.* Install Misskey *4.* Install Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
There is **two ways** to install Misskey:
### WAY 1) Using built code (recommended)
We have official release of Misskey.
The built code is automatically pushed to https://github.com/syuilo/misskey/tree/release after the CI test succeeds.
1. `git clone -b release git://github.com/syuilo/misskey.git`
2. `cd misskey`
3. `npm install`
#### Update
1. `git fetch`
2. `git reset --hard origin/release`
3. `npm install`
### WAY 2) Using source code
If you want to build Misskey manually, you can do it via the
`build` command after download the source code of Misskey and install dependencies:
1. `git clone -b master git://github.com/syuilo/misskey.git` 1. `git clone -b master git://github.com/syuilo/misskey.git`
2. `cd misskey` 2. `cd misskey`

View File

@ -37,6 +37,15 @@ Misskeyは以下のサブドメインを使います:
MisskeyはreCAPTCHAトークンを必要とします。 MisskeyはreCAPTCHAトークンを必要とします。
https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。
*(オプション)* VAPIDキーペアの生成
----------------------------------------------------------------
ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
``` shell
npm install web-push -g
web-push generate-vapid-keys
```
*3.* 依存関係をインストールする *3.* 依存関係をインストールする
---------------------------------------------------------------- ----------------------------------------------------------------
これらのソフトウェアをインストール・設定してください: これらのソフトウェアをインストール・設定してください:
@ -52,26 +61,6 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生
*4.* Misskeyのインストール *4.* Misskeyのインストール
---------------------------------------------------------------- ----------------------------------------------------------------
Misskeyをインストールするには**2つの方法**があります:
### 方法 1) ビルドされたコードを利用する (推奨)
Misskeyには公式のリリースがあります。
ビルドされたコードはCIテストに合格した後、自動で https://github.com/syuilo/misskey/tree/release にpushされています。
1. `git clone -b release git://github.com/syuilo/misskey.git`
2. `cd misskey`
3. `npm install`
#### アップデートするには:
1. `git fetch`
2. `git reset --hard origin/release`
3. `npm install`
### 方法 2) ソースコードを利用する
> 注: この方法では正しくビルド・動作できることは保証されません。
Misskeyを手動でビルドしたい場合は、Misskeyのソースコードと依存関係をインストールした後、
`build`コマンドを用いることができます:
1. `git clone -b master git://github.com/syuilo/misskey.git` 1. `git clone -b master git://github.com/syuilo/misskey.git`
2. `cd misskey` 2. `cd misskey`

View File

@ -13,7 +13,7 @@ import cssnano = require('gulp-cssnano');
import * as uglifyComposer from 'gulp-uglify/composer'; import * as uglifyComposer from 'gulp-uglify/composer';
import pug = require('gulp-pug'); import pug = require('gulp-pug');
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import * as chalk from 'chalk'; import chalk from 'chalk';
import imagemin = require('gulp-imagemin'); import imagemin = require('gulp-imagemin');
import * as rename from 'gulp-rename'; import * as rename from 'gulp-rename';
import * as mocha from 'gulp-mocha'; import * as mocha from 'gulp-mocha';
@ -81,9 +81,19 @@ gulp.task('lint', () =>
.pipe(tslint.report()) .pipe(tslint.report())
); );
gulp.task('format', () =>
gulp.src('./src/**/*.ts')
.pipe(tslint({
formatter: 'verbose',
fix: true
}))
.pipe(tslint.report())
);
gulp.task('mocha', () => gulp.task('mocha', () =>
gulp.src([]) gulp.src([])
.pipe(mocha({ .pipe(mocha({
exit: true
//compilers: 'ts:ts-node/register' //compilers: 'ts:ts-node/register'
} as any)) } as any))
); );
@ -123,7 +133,7 @@ gulp.task('build:client:script', () =>
.pipe(replace('VERSION', JSON.stringify(version))) .pipe(replace('VERSION', JSON.stringify(version)))
.pipe(isProduction ? uglify({ .pipe(isProduction ? uglify({
toplevel: true toplevel: true
}) : gutil.noop()) } as any) : gutil.noop())
.pipe(gulp.dest('./built/web/assets/')) as any .pipe(gulp.dest('./built/web/assets/')) as any
); );

View File

@ -13,6 +13,15 @@ common:
months_ago: "{}month(s) ago" months_ago: "{}month(s) ago"
years_ago: "{}year(s) ago" years_ago: "{}year(s) ago"
weekday-short:
sunday: "S"
monday: "M"
tuesday: "T"
wednesday: "W"
thursday: "T"
friday: "F"
satruday: "S"
reactions: reactions:
like: "Like" like: "Like"
love: "Love" love: "Love"
@ -41,6 +50,15 @@ common:
my-token-regenerated: "Your token is just regenerated, so you will signout." my-token-regenerated: "Your token is just regenerated, so you will signout."
tags: tags:
mk-nav-links:
about: "About"
stats: "Stats"
status: "Status"
wiki: "Wiki"
donors: "Donors"
repository: "Repository"
develop: "Developers"
mk-messaging-form: mk-messaging-form:
attach-from-local: "Attach file from your pc" attach-from-local: "Attach file from your pc"
attach-from-drive: "Attach file from the drive" attach-from-drive: "Attach file from the drive"
@ -225,7 +243,6 @@ desktop:
mk-drive-browser-file: mk-drive-browser-file:
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
wallpaper: "Wallpaper"
mk-drive-browser-folder-contextmenu: mk-drive-browser-folder-contextmenu:
move-to-this-folder: "Move to this folder" move-to-this-folder: "Move to this folder"
@ -242,14 +259,11 @@ desktop:
mk-drive-browser-nav-folder: mk-drive-browser-nav-folder:
drive: "Drive" drive: "Drive"
mk-nav-home-widget: mk-selectdrive-page:
about: "About" title: "Choose a file(s)"
stats: "Stats" ok: "OK"
status: "Status" cancel: "Cancel"
wiki: "Wiki" upload: "Upload a file(s) from you PC"
donors: "Donors"
repository: "Repository"
develop: "Developers"
mk-ui-header-nav: mk-ui-header-nav:
home: "Home" home: "Home"
@ -267,6 +281,12 @@ desktop:
settings: "Settings" settings: "Settings"
signout: "Sign out" signout: "Sign out"
mk-ui-header-post-button:
post: "Compose new Post"
mk-ui-header-notifications:
title: "Notifications"
mk-password-setting: mk-password-setting:
reset: "Change your password" reset: "Change your password"
enter-current-password: "Enter the current password" enter-current-password: "Enter the current password"
@ -327,7 +347,7 @@ desktop:
title: "Server info" title: "Server info"
toggle: "Toggle views" toggle: "Toggle views"
mk-activity-home-widget: mk-activity-widget:
title: "Activity" title: "Activity"
toggle: "Toggle views" toggle: "Toggle views"
@ -354,6 +374,34 @@ desktop:
title: "Donation" title: "Donation"
text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!" text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!"
mk-channel-home-widget:
title: "Channel"
settings: "Widget settings"
get-started: "Please click the cog in the upper right to specify the channel to receive"
mk-calendar-widget:
title: "{1} / {2}"
prev: "Previous month"
next: "Next month"
go: "Click to travel"
mk-post-form-home-widget:
title: "Post"
post: "Post"
placeholder: "What's happening?"
mk-access-log-home-widget:
title: "Access log"
mk-messaging-home-widget:
title: "Messaging"
mk-broadcast-home-widget:
fetching: "Fetching"
no-broadcasts: "No broadcasts"
have-a-nice-day: "Have a nice day!"
next: "Next"
mk-repost-form: mk-repost-form:
quote: "Quote..." quote: "Quote..."
cancel: "Cancel" cancel: "Cancel"
@ -365,6 +413,24 @@ desktop:
mk-repost-form-window: mk-repost-form-window:
title: "Are you sure you want to repost this post?" title: "Are you sure you want to repost this post?"
mk-user:
last-used-at: "Last used at"
photos:
title: "Photos"
loading: "Loading"
no-photos: "No photos"
frequently-replied-users:
title: "Frequently replied"
loading: "Loading"
no-users: "No users"
followers-you-know:
title: "Followers you know"
loading: "Loading"
no-users: "No users"
mobile: mobile:
tags: tags:
mk-selectdrive-page: mk-selectdrive-page:
@ -374,7 +440,7 @@ mobile:
download: "Download" download: "Download"
rename: "Rename" rename: "Rename"
move: "Move" move: "Move"
hash: "Hash" hash: "Hash (md5)"
mk-entrance-signin: mk-entrance-signin:
signup: "Sign up" signup: "Sign up"

View File

@ -13,6 +13,15 @@ common:
months_ago: "{}ヶ月前" months_ago: "{}ヶ月前"
years_ago: "{}年前" years_ago: "{}年前"
weekday-short:
sunday: "日"
monday: "月"
tuesday: "火"
wednesday: "水"
thursday: "木"
friday: "金"
satruday: "土"
reactions: reactions:
like: "いいね" like: "いいね"
love: "ハート" love: "ハート"
@ -41,6 +50,15 @@ common:
my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
tags: tags:
mk-nav-links:
about: "Misskeyについて"
stats: "統計"
status: "ステータス"
wiki: "Wiki"
donors: "ドナー"
repository: "リポジトリ"
develop: "開発者"
mk-messaging-form: mk-messaging-form:
attach-from-local: "PCからファイルを添付する" attach-from-local: "PCからファイルを添付する"
attach-from-drive: "ドライブからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する"
@ -225,7 +243,6 @@ desktop:
mk-drive-browser-file: mk-drive-browser-file:
avatar: "アバター" avatar: "アバター"
banner: "バナー" banner: "バナー"
wallpaper: "壁紙"
mk-drive-browser-folder-contextmenu: mk-drive-browser-folder-contextmenu:
move-to-this-folder: "このフォルダへ移動" move-to-this-folder: "このフォルダへ移動"
@ -242,14 +259,11 @@ desktop:
mk-drive-browser-nav-folder: mk-drive-browser-nav-folder:
drive: "ドライブ" drive: "ドライブ"
mk-nav-home-widget: mk-selectdrive-page:
about: "Misskeyについて" title: "ファイルを選択してください"
stats: "統計" ok: "決定"
status: "ステータス" cancel: "キャンセル"
wiki: "Wiki" upload: "PCからドライブにファイルをアップロード"
donors: "ドナー"
repository: "リポジトリ"
develop: "開発者"
mk-ui-header-nav: mk-ui-header-nav:
home: "ホーム" home: "ホーム"
@ -267,6 +281,12 @@ desktop:
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "サインアウト"
mk-ui-header-post-button:
post: "新規投稿"
mk-ui-header-notifications:
title: "通知"
mk-password-setting: mk-password-setting:
reset: "パスワードを変更する" reset: "パスワードを変更する"
enter-current-password: "現在のパスワードを入力してください" enter-current-password: "現在のパスワードを入力してください"
@ -327,7 +347,7 @@ desktop:
title: "サーバー情報" title: "サーバー情報"
toggle: "表示を切り替え" toggle: "表示を切り替え"
mk-activity-home-widget: mk-activity-widget:
title: "アクティビティ" title: "アクティビティ"
toggle: "表示を切り替え" toggle: "表示を切り替え"
@ -354,6 +374,34 @@ desktop:
title: "寄付のお願い" title: "寄付のお願い"
text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。" text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。"
mk-channel-home-widget:
title: "チャンネル"
settings: "ウィジェットの設定"
get-started: "右上の歯車をクリックして受信するチャンネルを指定してください"
mk-calendar-widget:
title: "{1}年 {2}月"
prev: "先月"
next: "来月"
go: "クリックして時間遡行"
mk-post-form-home-widget:
title: "投稿"
post: "投稿"
placeholder: "いまどうしてる?"
mk-access-log-home-widget:
title: "アクセスログ"
mk-messaging-home-widget:
title: "メッセージ"
mk-broadcast-home-widget:
fetching: "確認中"
no-broadcasts: "お知らせはありません"
have-a-nice-day: "良い一日を!"
next: "次"
mk-repost-form: mk-repost-form:
quote: "引用する..." quote: "引用する..."
cancel: "キャンセル" cancel: "キャンセル"
@ -365,6 +413,24 @@ desktop:
mk-repost-form-window: mk-repost-form-window:
title: "この投稿をRepostしますか" title: "この投稿をRepostしますか"
mk-user:
last-used-at: "最終アクセス"
photos:
title: "フォト"
loading: "読み込み中"
no-photos: "写真はありません"
frequently-replied-users:
title: "よく話すユーザー"
loading: "読み込み中"
no-users: "よく話すユーザーはいません"
followers-you-know:
title: "知り合いのフォロワー"
loading: "読み込み中"
no-users: "知り合いのフォロワーはいません"
mobile: mobile:
tags: tags:
mk-selectdrive-page: mk-selectdrive-page:
@ -374,7 +440,7 @@ mobile:
download: "ダウンロード" download: "ダウンロード"
rename: "名前を変更" rename: "名前を変更"
move: "移動" move: "移動"
hash: "ハッシュ" hash: "ハッシュ (md5)"
mk-entrance-signin: mk-entrance-signin:
signup: "新規登録" signup: "新規登録"

View File

@ -1,158 +1,166 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "0.0.2807", "version": "0.0.3201",
"license": "MIT", "license": "MIT",
"description": "A miniblog-based SNS", "description": "A miniblog-based SNS",
"bugs": "https://github.com/syuilo/misskey/issues", "bugs": "https://github.com/syuilo/misskey/issues",
"repository": "https://github.com/syuilo/misskey.git", "repository": "https://github.com/syuilo/misskey.git",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
"scripts": { "scripts": {
"config": "node ./tools/init.js", "config": "node ./tools/init.js",
"start": "node ./built", "start": "node ./built",
"debug": "DEBUG=misskey:* node ./built", "debug": "DEBUG=misskey:* node ./built",
"swagger": "node ./swagger.js", "swagger": "node ./swagger.js",
"build": "gulp build", "build": "gulp build",
"rebuild": "gulp rebuild", "rebuild": "gulp rebuild",
"clean": "gulp clean", "clean": "gulp clean",
"cleanall": "gulp cleanall", "cleanall": "gulp cleanall",
"lint": "gulp lint", "lint": "gulp lint",
"test": "gulp test" "test": "gulp test",
}, "format": "gulp format"
"devDependencies": { },
"@types/bcryptjs": "2.4.0", "dependencies": {
"@types/body-parser": "1.16.5", "@prezzemolo/rap": "0.1.2",
"@types/chai": "4.0.4", "@prezzemolo/zip": "0.0.3",
"@types/chai-http": "3.0.3", "@types/bcryptjs": "2.4.1",
"@types/chalk": "0.4.31", "@types/body-parser": "1.16.8",
"@types/compression": "0.0.34", "@types/chai": "4.0.5",
"@types/cors": "2.8.1", "@types/chai-http": "3.0.3",
"@types/debug": "0.0.30", "@types/compression": "0.0.35",
"@types/deep-equal": "1.0.1", "@types/cookie": "0.3.1",
"@types/elasticsearch": "5.0.14", "@types/cors": "2.8.3",
"@types/event-stream": "3.3.32", "@types/debug": "0.0.30",
"@types/express": "4.0.37", "@types/deep-equal": "1.0.1",
"@types/gm": "1.17.32", "@types/elasticsearch": "5.0.17",
"@types/gulp": "4.0.3", "@types/event-stream": "3.3.33",
"@types/gulp-htmlmin": "1.3.30", "@types/eventemitter3": "2.0.2",
"@types/gulp-mocha": "0.0.30", "@types/express": "4.0.39",
"@types/gulp-rename": "0.0.32", "@types/gm": "1.17.33",
"@types/gulp-replace": "0.0.30", "@types/gulp": "4.0.3",
"@types/gulp-tslint": "3.6.31", "@types/gulp-htmlmin": "1.3.31",
"@types/gulp-typescript": "2.13.0", "@types/gulp-mocha": "0.0.31",
"@types/gulp-uglify": "0.0.30", "@types/gulp-rename": "0.0.33",
"@types/gulp-util": "3.0.31", "@types/gulp-replace": "0.0.31",
"@types/inquirer": "0.0.34", "@types/gulp-uglify": "3.0.3",
"@types/is-root": "1.0.0", "@types/gulp-util": "3.0.34",
"@types/is-url": "1.2.28", "@types/inquirer": "0.0.35",
"@types/js-yaml": "3.9.0", "@types/is-root": "1.0.0",
"@types/mocha": "2.2.43", "@types/is-url": "1.2.28",
"@types/mongodb": "2.2.13", "@types/js-yaml": "3.10.0",
"@types/monk": "1.0.6", "@types/mocha": "2.2.44",
"@types/morgan": "1.7.33", "@types/mongodb": "2.2.15",
"@types/ms": "0.7.30", "@types/monk": "1.0.6",
"@types/multer": "1.3.2", "@types/morgan": "1.7.35",
"@types/node": "8.0.33", "@types/ms": "0.7.30",
"@types/ratelimiter": "2.1.28", "@types/multer": "1.3.6",
"@types/redis": "2.6.0", "@types/node": "8.0.53",
"@types/request": "2.0.4", "@types/page": "1.5.32",
"@types/rimraf": "2.0.0", "@types/proxy-addr": "2.0.0",
"@types/riot": "3.6.0", "@types/ratelimiter": "2.1.28",
"@types/serve-favicon": "2.2.28", "@types/redis": "2.8.1",
"@types/uuid": "3.4.2", "@types/request": "2.0.7",
"@types/webpack": "3.0.13", "@types/rimraf": "2.0.2",
"@types/webpack-stream": "3.2.7", "@types/riot": "3.6.1",
"@types/websocket": "0.0.34", "@types/seedrandom": "2.4.27",
"awesome-typescript-loader": "3.2.3", "@types/serve-favicon": "2.2.30",
"chai": "4.1.2", "@types/tmp": "0.0.33",
"chai-http": "3.0.0", "@types/uuid": "3.4.3",
"css-loader": "0.28.7", "@types/webpack": "3.8.1",
"event-stream": "3.3.4", "@types/webpack-stream": "3.2.8",
"gulp": "3.9.1", "@types/websocket": "0.0.34",
"gulp-cssnano": "2.1.2", "accesses": "2.5.0",
"gulp-htmlmin": "3.0.0", "animejs": "2.2.0",
"gulp-imagemin": "3.4.0", "autwh": "0.0.1",
"gulp-mocha": "4.3.1", "awesome-typescript-loader": "3.4.0",
"gulp-pug": "3.3.0", "bcryptjs": "2.4.3",
"gulp-rename": "1.2.2", "body-parser": "1.18.2",
"gulp-replace": "0.6.1", "cafy": "3.2.0",
"gulp-tslint": "8.1.2", "chai": "4.1.2",
"gulp-typescript": "3.2.2", "chai-http": "3.0.0",
"gulp-uglify": "3.0.0", "chalk": "2.3.0",
"gulp-util": "3.0.8", "compression": "1.7.1",
"mocha": "3.5.3", "cookie": "0.3.1",
"riot-tag-loader": "1.0.0", "cors": "2.8.4",
"string-replace-webpack-plugin": "0.1.3", "cropperjs": "1.1.3",
"style-loader": "0.19.0", "css-loader": "0.28.7",
"stylus": "0.54.5", "debug": "3.1.0",
"stylus-loader": "3.0.1", "deep-equal": "1.0.1",
"swagger-jsdoc": "1.9.7", "deepcopy": "0.6.3",
"tslint": "5.7.0", "diskusage": "0.2.4",
"uglify-es": "3.0.27", "elasticsearch": "14.0.0",
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", "escape-regexp": "0.0.1",
"uglifyjs-webpack-plugin": "1.0.0-beta.2", "event-stream": "3.3.4",
"webpack": "3.8.1" "eventemitter3": "2.0.3",
}, "express": "4.16.2",
"dependencies": { "file-type": "7.3.0",
"accesses": "2.5.0", "fuckadblock": "3.2.1",
"animejs": "2.2.0", "gm": "1.23.0",
"autwh": "0.0.1", "gulp": "3.9.1",
"bcryptjs": "2.4.3", "gulp-cssnano": "2.1.2",
"body-parser": "1.18.2", "gulp-htmlmin": "3.0.0",
"cafy": "3.0.0", "gulp-imagemin": "4.0.0",
"chalk": "2.1.0", "gulp-mocha": "4.3.1",
"compression": "1.7.1", "gulp-pug": "3.3.0",
"cors": "2.8.4", "gulp-rename": "1.2.2",
"cropperjs": "1.1.3", "gulp-replace": "0.6.1",
"crypto": "1.0.1", "gulp-tslint": "8.1.2",
"debug": "3.1.0", "gulp-typescript": "3.2.3",
"deep-equal": "1.0.1", "gulp-uglify": "3.0.0",
"deepcopy": "0.6.3", "gulp-util": "3.0.8",
"diskusage": "0.2.2", "inquirer": "4.0.0",
"download": "6.2.5", "is-root": "1.0.0",
"elasticsearch": "13.3.1", "is-url": "1.2.2",
"escape-regexp": "0.0.1", "js-yaml": "3.10.0",
"express": "4.15.4", "mecab-async": "0.1.0",
"file-type": "6.2.0", "mocha": "4.0.1",
"fuckadblock": "3.2.1", "moji": "0.5.1",
"gm": "1.23.0", "mongodb": "2.2.33",
"inquirer": "3.3.0", "monk": "6.0.5",
"is-root": "1.0.0", "morgan": "1.9.0",
"is-url": "1.2.2", "ms": "2.0.0",
"js-yaml": "3.10.0", "multer": "1.3.0",
"mecab-async": "^0.1.0", "nprogress": "0.2.0",
"moji": "^0.5.1", "os-utils": "0.0.14",
"mongodb": "2.2.33", "page": "1.7.1",
"monk": "6.0.5", "pictograph": "2.1.2",
"morgan": "1.9.0", "prominence": "0.2.0",
"ms": "2.0.0", "proxy-addr": "2.0.2",
"multer": "1.3.0", "pug": "2.0.0-rc.4",
"nprogress": "0.2.0", "ratelimiter": "3.0.3",
"os-utils": "0.0.14", "recaptcha-promise": "0.1.3",
"page": "1.7.1", "reconnecting-websocket": "3.2.2",
"pictograph": "2.0.4", "redis": "2.8.0",
"prominence": "0.2.0", "request": "2.83.0",
"pug": "2.0.0-rc.4", "rimraf": "2.6.2",
"ratelimiter": "3.0.3", "riot": "3.7.4",
"recaptcha-promise": "0.1.3", "riot-tag-loader": "1.0.0",
"reconnecting-websocket": "3.2.2", "rndstr": "1.0.0",
"redis": "2.8.0", "s-age": "1.1.0",
"request": "2.83.0", "seedrandom": "^2.4.3",
"rimraf": "2.6.2", "serve-favicon": "2.4.5",
"riot": "3.7.3", "sortablejs": "1.7.0",
"rndstr": "1.0.0", "string-replace-webpack-plugin": "0.1.3",
"s-age": "1.1.0", "style-loader": "0.19.0",
"serve-favicon": "2.4.5", "stylus": "0.54.5",
"summaly": "2.0.3", "stylus-loader": "3.0.1",
"syuilo-password-strength": "0.0.1", "summaly": "2.0.3",
"tcp-port-used": "0.1.2", "swagger-jsdoc": "1.9.7",
"textarea-caret": "3.0.2", "syuilo-password-strength": "0.0.1",
"ts-node": "3.3.0", "tcp-port-used": "0.1.2",
"typescript": "2.5.3", "textarea-caret": "3.0.2",
"uuid": "3.1.0", "tmp": "0.0.33",
"vhost": "3.0.2", "ts-node": "3.3.0",
"websocket": "1.0.25", "tslint": "5.8.0",
"xev": "2.0.0" "typescript": "2.6.1",
} "uglify-es": "3.1.10",
"uglifyjs-webpack-plugin": "1.1.1",
"uuid": "3.1.0",
"vhost": "3.0.2",
"web-push": "3.2.4",
"webpack": "3.8.1",
"websocket": "1.0.25",
"xev": "2.0.0"
}
} }

View File

@ -5,6 +5,7 @@ import User, { IUser, init as initUser } from '../models/user';
import getPostSummary from '../../common/get-post-summary'; import getPostSummary from '../../common/get-post-summary';
import getUserSummary from '../../common/get-user-summary'; import getUserSummary from '../../common/get-user-summary';
import getNotificationSummary from '../../common/get-notification-summary';
import Othello, { ai as othelloAi } from '../../common/othello'; import Othello, { ai as othelloAi } from '../../common/othello';
@ -62,7 +63,7 @@ export default class BotCore extends EventEmitter {
return bot; return bot;
} }
public async q(query: string): Promise<string | void> { public async q(query: string): Promise<string> {
if (this.context != null) { if (this.context != null) {
return await this.context.q(query); return await this.context.q(query);
} }
@ -84,7 +85,10 @@ export default class BotCore extends EventEmitter {
'logout, signout: サインアウトします\n' + 'logout, signout: サインアウトします\n' +
'post: 投稿します\n' + 'post: 投稿します\n' +
'tl: タイムラインを見ます\n' + 'tl: タイムラインを見ます\n' +
'@<ユーザー名>: ユーザーを表示します'; 'no: 通知を見ます\n' +
'@<ユーザー名>: ユーザーを表示します\n' +
'\n' +
'タイムラインや通知を見た後、「次」というとさらに遡ることができます。';
case 'me': case 'me':
return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
@ -113,7 +117,16 @@ export default class BotCore extends EventEmitter {
case 'tl': case 'tl':
case 'タイムライン': case 'タイムライン':
return await this.tlCommand(); if (this.user == null) return 'まずサインインしてください。';
this.setContext(new TlContext(this));
return await this.context.greet();
case 'no':
case 'notifications':
case '通知':
if (this.user == null) return 'まずサインインしてください。';
this.setContext(new NotificationsContext(this));
return await this.context.greet();
case 'guessing-game': case 'guessing-game':
case '数当てゲーム': case '数当てゲーム':
@ -155,21 +168,7 @@ export default class BotCore extends EventEmitter {
this.emit('updated'); this.emit('updated');
} }
public async tlCommand(): Promise<string | void> { public async showUserCommand(q: string): Promise<string> {
if (this.user == null) return 'まずサインインしてください。';
const tl = await require('../endpoints/posts/timeline')({
limit: 5
}, this.user);
const text = tl
.map(post => getPostSummary(post))
.join('\n-----\n');
return text;
}
public async showUserCommand(q: string): Promise<string | void> {
try { try {
const user = await require('../endpoints/users/show')({ const user = await require('../endpoints/users/show')({
username: q.substr(1) username: q.substr(1)
@ -200,6 +199,8 @@ abstract class Context extends EventEmitter {
if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content); if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
if (data.type == 'othello') return OthelloContext.import(bot, data.content); if (data.type == 'othello') return OthelloContext.import(bot, data.content);
if (data.type == 'post') return PostContext.import(bot, data.content); if (data.type == 'post') return PostContext.import(bot, data.content);
if (data.type == 'tl') return TlContext.import(bot, data.content);
if (data.type == 'notifications') return NotificationsContext.import(bot, data.content);
if (data.type == 'signin') return SigninContext.import(bot, data.content); if (data.type == 'signin') return SigninContext.import(bot, data.content);
return null; return null;
} }
@ -232,7 +233,7 @@ class SigninContext extends Context {
} }
} else { } else {
// Compare password // Compare password
const same = bcrypt.compareSync(query, this.temporaryUser.password); const same = await bcrypt.compare(query, this.temporaryUser.password);
if (same) { if (same) {
this.bot.signin(this.temporaryUser); this.bot.signin(this.temporaryUser);
@ -285,6 +286,110 @@ class PostContext extends Context {
} }
} }
class TlContext extends Context {
private next: string = null;
public async greet(): Promise<string> {
return await this.getTl();
}
public async q(query: string): Promise<string> {
if (query == '次') {
return await this.getTl();
} else {
this.bot.clearContext();
return await this.bot.q(query);
}
}
private async getTl() {
const tl = await require('../endpoints/posts/timeline')({
limit: 5,
max_id: this.next ? this.next : undefined
}, this.bot.user);
if (tl.length > 0) {
this.next = tl[tl.length - 1].id;
this.emit('updated');
const text = tl
.map(post => `${post.user.name}\n「${getPostSummary(post)}`)
.join('\n-----\n');
return text;
} else {
return 'タイムラインに表示するものがありません...';
}
}
public export() {
return {
type: 'tl',
content: {
next: this.next,
}
};
}
public static import(bot: BotCore, data: any) {
const context = new TlContext(bot);
context.next = data.next;
return context;
}
}
class NotificationsContext extends Context {
private next: string = null;
public async greet(): Promise<string> {
return await this.getNotifications();
}
public async q(query: string): Promise<string> {
if (query == '次') {
return await this.getNotifications();
} else {
this.bot.clearContext();
return await this.bot.q(query);
}
}
private async getNotifications() {
const notifications = await require('../endpoints/i/notifications')({
limit: 5,
max_id: this.next ? this.next : undefined
}, this.bot.user);
if (notifications.length > 0) {
this.next = notifications[notifications.length - 1].id;
this.emit('updated');
const text = notifications
.map(notification => getNotificationSummary(notification))
.join('\n-----\n');
return text;
} else {
return '通知はありません';
}
}
public export() {
return {
type: 'notifications',
content: {
next: this.next,
}
};
}
public static import(bot: BotCore, data: any) {
const context = new NotificationsContext(bot);
context.next = data.next;
return context;
}
}
class GuessingGameContext extends Context { class GuessingGameContext extends Context {
private secret: number; private secret: number;
private history: number[] = []; private history: number[] = [];

View File

@ -135,6 +135,8 @@ class LineBot extends BotCore {
actions: actions actions: actions
} }
}]); }]);
return null;
} }
public async showUserTimelinePostback(userId: string) { 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 mongodb from 'mongodb';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as gm from 'gm'; import * as gm from 'gm';
import * as debug from 'debug'; import * as debug from 'debug';
import fileType = require('file-type'); import fileType = require('file-type');
import prominence = require('prominence'); import prominence = require('prominence');
import DriveFile from '../models/drive-file';
import DriveFile, { getGridFSBucket } from '../models/drive-file';
import DriveFolder from '../models/drive-folder'; import DriveFolder from '../models/drive-folder';
import serialize from '../serializers/drive-file'; import serialize from '../serializers/drive-file';
import event from '../event'; import event, { publishDriveStream } from '../event';
import config from '../../conf'; import config from '../../conf';
const log = debug('misskey:register-drive-file'); const log = debug('misskey:register-drive-file');
const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
tmp.file((e, path) => {
if (e) return reject(e);
resolve(path);
});
});
const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> =>
getGridFSBucket()
.then(bucket => new Promise((resolve, reject) => {
const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
writeStream.once('finish', (doc) => { resolve(doc); });
writeStream.on('error', reject);
readable.pipe(writeStream);
}));
const addFile = async (
user: any,
path: string,
name: string = null,
comment: string = null,
folderId: mongodb.ObjectID = null,
force: boolean = false
) => {
log(`registering ${name} (user: ${user.username}, path: ${path})`);
// Calculate hash, get content type and get file size
const [hash, [mime, ext], size] = await Promise.all([
// hash
((): Promise<string> => new Promise((res, rej) => {
const readable = fs.createReadStream(path);
const hash = crypto.createHash('md5');
const chunks = [];
readable
.on('error', rej)
.pipe(hash)
.on('error', rej)
.on('data', (chunk) => chunks.push(chunk))
.on('end', () => {
const buffer = Buffer.concat(chunks);
res(buffer.toString('hex'));
});
}))(),
// mime
((): Promise<[string, string | null]> => new Promise((res, rej) => {
const readable = fs.createReadStream(path);
readable
.on('error', rej)
.once('data', (buffer: Buffer) => {
readable.destroy();
const type = fileType(buffer);
if (type) {
return res([type.mime, type.ext]);
} else {
// 種類が同定できなかったら application/octet-stream にする
return res(['application/octet-stream', null]);
}
});
}))(),
// size
((): Promise<number> => new Promise((res, rej) => {
fs.stat(path, (err, stats) => {
if (err) return rej(err);
res(stats.size);
});
}))()
]);
log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
// detect name
const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
if (!force) {
// Check if there is a file with the same hash
const much = await DriveFile.findOne({
md5: hash,
'metadata.user_id': user._id
});
if (much !== null) {
log('file with same hash is found');
return much;
} else {
log('file with same hash is not found');
}
}
const [properties, folder] = await Promise.all([
// properties
(async () => {
// 画像かどうか
if (!/^image\/.*$/.test(mime)) {
return null;
}
const imageType = mime.split('/')[1];
// 画像でもPNGかJPEGでないならスキップ
if (imageType != 'png' && imageType != 'jpeg') {
return null;
}
// If the file is an image, calculate width and height to save in property
const g = gm(fs.createReadStream(path), name);
const size = await prominence(g).size();
const properties = {
width: size.width,
height: size.height
};
log('image width and height is calculated');
return properties;
})(),
// folder
(async () => {
if (!folderId) {
return null;
}
const driveFolder = await DriveFolder.findOne({
_id: folderId,
user_id: user._id
});
if (!driveFolder) {
throw 'folder-not-found';
}
return driveFolder;
})(),
// usage checker
(async () => {
// Calculate drive usage
const usage = await DriveFile
.aggregate([{
$match: { 'metadata.user_id': user._id }
}, {
$project: {
length: true
}
}, {
$group: {
_id: null,
usage: { $sum: '$length' }
}
}])
.then((aggregates: any[]) => {
if (aggregates.length > 0) {
return aggregates[0].usage;
}
return 0;
});
log(`drive usage is ${usage}`);
// If usage limit exceeded
if (usage + size > user.drive_capacity) {
throw 'no-free-space';
}
})()
]);
const readable = fs.createReadStream(path);
return addToGridFS(detectedName, readable, mime, {
user_id: user._id,
folder_id: folder !== null ? folder._id : null,
comment: comment,
properties: properties
});
};
/** /**
* Add file to drive * Add file to drive
* *
* @param user User who wish to add file * @param user User who wish to add file
* @param fileName File name * @param file File path or readableStream
* @param data Contents
* @param comment Comment * @param comment Comment
* @param type File type * @param type File type
* @param folderId Folder ID * @param folderId Folder ID
* @param force If set to true, forcibly upload the file even if there is a file with the same hash. * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
* @return Object that represents added file * @return Object that represents added file
*/ */
export default ( export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
user: any, // Get file path
data: Buffer, new Promise((res: (v: [string, boolean]) => void, rej) => {
name: string = null, if (typeof file === 'string') {
comment: string = null, res([file, false]);
folderId: mongodb.ObjectID = null, return;
force: boolean = false
) => new Promise<any>(async (resolve, reject) => {
log(`registering ${name} (user: ${user.username})`);
// File size
const size = data.byteLength;
log(`size is ${size}`);
// File type
let mime = 'application/octet-stream';
const type = fileType(data);
if (type !== null) {
mime = type.mime;
if (name === null) {
name = `untitled.${type.ext}`;
} }
} else { if (typeof file === 'object' && typeof file.read === 'function') {
if (name === null) { tmpFile()
name = 'untitled'; .then(path => {
const readable: stream.Readable = file;
const writable = fs.createWriteStream(path);
readable
.on('error', rej)
.on('end', () => {
res([path, true]);
})
.pipe(writable)
.on('error', rej);
})
.catch(rej);
} }
} rej(new Error('un-compatible file.'));
})
.then(([path, remove]): Promise<any> => new Promise((res, rej) => {
addFile(user, path, ...args)
.then(file => {
res(file);
if (remove) {
fs.unlink(path, (e) => {
if (e) log(e.stack);
});
}
})
.catch(rej);
}))
.then(file => {
log(`drive file has been created ${file._id}`);
resolve(file);
log(`type is ${mime}`); serialize(file).then(serializedFile => {
// Publish drive_file_created event
event(user._id, 'drive_file_created', serializedFile);
publishDriveStream(user._id, 'file_created', serializedFile);
// Generate hash // Register to search database
const hash = crypto if (config.elasticsearch.enable) {
.createHash('sha256') const es = require('../../db/elasticsearch');
.update(data) es.index({
.digest('hex') as string; index: 'misskey',
type: 'drive_file',
log(`hash is ${hash}`); id: file._id.toString(),
body: {
if (!force) { name: file.name,
// Check if there is a file with the same hash user_id: user._id.toString()
const much = await DriveFile.findOne({ }
user_id: user._id, });
hash: hash
});
if (much !== null) {
log('file with same hash is found');
return resolve(much);
} else {
log('file with same hash is not found');
}
}
// Calculate drive usage
const usage = ((await DriveFile
.aggregate([
{ $match: { user_id: user._id } },
{ $project: {
datasize: true
}},
{ $group: {
_id: null,
usage: { $sum: '$datasize' }
}}
]))[0] || {
usage: 0
}).usage;
log(`drive usage is ${usage}`);
// If usage limit exceeded
if (usage + size > user.drive_capacity) {
return reject('no-free-space');
}
// If the folder is specified
let folder: any = null;
if (folderId !== null) {
folder = await DriveFolder
.findOne({
_id: folderId,
user_id: user._id
});
if (folder === null) {
return reject('folder-not-found');
}
}
let properties: any = null;
// If the file is an image
if (/^image\/.*$/.test(mime)) {
// Calculate width and height to save in property
const g = gm(data, name);
const size = await prominence(g).size();
properties = {
width: size.width,
height: size.height
};
log('image width and height is calculated');
}
// Create DriveFile document
const file = await DriveFile.insert({
created_at: new Date(),
user_id: user._id,
folder_id: folder !== null ? folder._id : null,
data: data,
datasize: size,
type: mime,
name: name,
comment: comment,
hash: hash,
properties: properties
});
delete file.data;
log(`drive file has been created ${file._id}`);
resolve(file);
// Serialize
const fileObj = await serialize(file);
// Publish drive_file_created event
event(user._id, 'drive_file_created', fileObj);
// Register to search database
if (config.elasticsearch.enable) {
const es = require('../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'drive_file',
id: file._id.toString(),
body: {
name: file.name,
user_id: user._id.toString()
} }
}); });
} })
.catch(reject);
}); });

View File

@ -27,4 +27,12 @@ export default (
// Publish notification event // Publish notification event
event(notifiee, 'notification', event(notifiee, 'notification',
await serialize(notification)); await serialize(notification));
// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => {
const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
if (!fresh.is_read) {
event(notifiee, 'unread_notification', await serialize(notification));
}
}, 3000);
}); });

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 { IMessagingMessage as IMessage } from '../models/messaging-message';
import publishUserStream from '../event'; import publishUserStream from '../event';
import { publishMessagingStream } from '../event'; import { publishMessagingStream } from '../event';
import { publishMessagingIndexStream } from '../event';
/** /**
* Mark as read message(s) * Mark as read message(s)
@ -49,6 +50,7 @@ export default (
// Publish event // Publish event
publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString()));
// Calc count of my unread messages // Calc count of my unread messages
const count = await Message const count = await Message

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: 'aggregation/posts/reactions'
}, },
{
name: 'sw/register',
withCredential: true
},
{ {
name: 'i', name: 'i',
withCredential: true withCredential: true
@ -159,6 +164,11 @@ const endpoints: Endpoint[] = [
}, },
kind: 'account-write' kind: 'account-write'
}, },
{
name: 'i/update_home',
withCredential: true,
kind: 'account-write'
},
{ {
name: 'i/change_password', name: 'i/change_password',
withCredential: true withCredential: true

View File

@ -85,7 +85,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
if (permissionErr) return rej('invalid permission param'); if (permissionErr) return rej('invalid permission param');
// Get 'callback_url' parameter // Get 'callback_url' parameter
// TODO: Check $ is valid url // TODO: Check it is valid url
const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$; const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$;
if (callbackUrlErr) return rej('invalid callback_url param'); if (callbackUrlErr) return rej('invalid callback_url param');

View File

@ -3,7 +3,7 @@
*/ */
import $ from 'cafy'; import $ from 'cafy';
import { default as Channel, IChannel } from '../../models/channel'; import { default as Channel, IChannel } from '../../models/channel';
import { default as Post, IPost } from '../../models/post'; import Post from '../../models/post';
import serialize from '../../serializers/post'; import serialize from '../../serializers/post';
/** /**

View File

@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Calculate drive usage // Calculate drive usage
const usage = ((await DriveFile const usage = ((await DriveFile
.aggregate([ .aggregate([
{ $match: { user_id: user._id } }, { $match: { 'metadata.user_id': user._id } },
{ {
$project: { $project: {
datasize: true length: true
} }
}, },
{ {
$group: { $group: {
_id: null, _id: null,
usage: { $sum: '$datasize' } usage: { $sum: '$length' }
} }
} }
]))[0] || { ]))[0] || {

View File

@ -13,35 +13,39 @@ import serialize from '../../serializers/drive-file';
* @param {any} app * @param {any} app
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user, app) => new Promise(async (res, rej) => { module.exports = async (params, user, app) => {
// Get 'limit' parameter // Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param'); if (limitErr) throw 'invalid limit param';
// Get 'since_id' parameter // Get 'since_id' parameter
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param'); if (sinceIdErr) throw 'invalid since_id param';
// Get 'max_id' parameter // Get 'max_id' parameter
const [maxId, maxIdErr] = $(params.max_id).optional.id().$; const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
if (maxIdErr) return rej('invalid max_id param'); if (maxIdErr) throw 'invalid max_id param';
// Check if both of since_id and max_id is specified // Check if both of since_id and max_id is specified
if (sinceId && maxId) { if (sinceId && maxId) {
return rej('cannot set since_id and max_id'); throw 'cannot set since_id and max_id';
} }
// Get 'folder_id' parameter // Get 'folder_id' parameter
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
if (folderIdErr) return rej('invalid folder_id param'); if (folderIdErr) throw 'invalid folder_id param';
// Get 'type' parameter
const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$;
if (typeErr) throw 'invalid type param';
// Construct query // Construct query
const sort = { const sort = {
_id: -1 _id: -1
}; };
const query = { const query = {
user_id: user._id, 'metadata.user_id': user._id,
folder_id: folderId 'metadata.folder_id': folderId
} as any; } as any;
if (sinceId) { if (sinceId) {
sort._id = 1; sort._id = 1;
@ -53,18 +57,18 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
$lt: maxId $lt: maxId
}; };
} }
if (type) {
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
}
// Issue query // Issue query
const files = await DriveFile const files = await DriveFile
.find(query, { .find(query, {
fields: {
data: false
},
limit: limit, limit: limit,
sort: sort sort: sort
}); });
// Serialize // Serialize
res(await Promise.all(files.map(async file => const _files = await Promise.all(files.map(file => serialize(file)));
await serialize(file)))); return _files;
}); };

View File

@ -1,7 +1,6 @@
/** /**
* Module dependencies * Module dependencies
*/ */
import * as fs from 'fs';
import $ from 'cafy'; import $ from 'cafy';
import { validateFileName } from '../../../models/drive-file'; import { validateFileName } from '../../../models/drive-file';
import serialize from '../../../serializers/drive-file'; import serialize from '../../../serializers/drive-file';
@ -15,14 +14,11 @@ import create from '../../../common/add-file-to-drive';
* @param {any} user * @param {any} user
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (file, params, user) => new Promise(async (res, rej) => { module.exports = async (file, params, user): Promise<any> => {
if (file == null) { if (file == null) {
return rej('file is required'); throw 'file is required';
} }
const buffer = fs.readFileSync(file.path);
fs.unlink(file.path, (err) => { if (err) console.log(err); });
// Get 'name' parameter // Get 'name' parameter
let name = file.originalname; let name = file.originalname;
if (name !== undefined && name !== null) { if (name !== undefined && name !== null) {
@ -32,7 +28,7 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
} else if (name === 'blob') { } else if (name === 'blob') {
name = null; name = null;
} else if (!validateFileName(name)) { } else if (!validateFileName(name)) {
return rej('invalid name'); throw 'invalid name';
} }
} else { } else {
name = null; name = null;
@ -40,14 +36,11 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => {
// Get 'folder_id' parameter // Get 'folder_id' parameter
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
if (folderIdErr) return rej('invalid folder_id param'); if (folderIdErr) throw 'invalid folder_id param';
// Create file // Create file
const driveFile = await create(user, buffer, name, null, folderId); const driveFile = await create(user, file.path, name, null, folderId);
// Serialize // Serialize
const fileObj = await serialize(driveFile); return serialize(driveFile);
};
// Response
res(fileObj);
});

View File

@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Issue query // Issue query
const files = await DriveFile const files = await DriveFile
.find({ .find({
name: name, filename: name,
user_id: user._id, 'metadata.user_id': user._id,
folder_id: folderId 'metadata.folder_id': folderId
}, {
fields: {
data: false
}
}); });
// Serialize // Serialize

View File

@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file';
* @param {any} user * @param {any} user
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user) => new Promise(async (res, rej) => { module.exports = async (params, user) => {
// Get 'file_id' parameter // Get 'file_id' parameter
const [fileId, fileIdErr] = $(params.file_id).id().$; const [fileId, fileIdErr] = $(params.file_id).id().$;
if (fileIdErr) return rej('invalid file_id param'); if (fileIdErr) throw 'invalid file_id param';
// Fetch file // Fetch file
const file = await DriveFile const file = await DriveFile
.findOne({ .findOne({
_id: fileId, _id: fileId,
user_id: user._id 'metadata.user_id': user._id
}, {
fields: {
data: false
}
}); });
if (file === null) { if (file === null) {
return rej('file-not-found'); throw 'file-not-found';
} }
// Serialize // Serialize
res(await serialize(file, { const _file = await serialize(file, {
detail: true detail: true
})); });
});
return _file;
};

View File

@ -6,7 +6,7 @@ import DriveFolder from '../../../models/drive-folder';
import DriveFile from '../../../models/drive-file'; import DriveFile from '../../../models/drive-file';
import { validateFileName } from '../../../models/drive-file'; import { validateFileName } from '../../../models/drive-file';
import serialize from '../../../serializers/drive-file'; import serialize from '../../../serializers/drive-file';
import event from '../../../event'; import { publishDriveStream } from '../../../event';
/** /**
* Update a file * Update a file
@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const file = await DriveFile const file = await DriveFile
.findOne({ .findOne({
_id: fileId, _id: fileId,
user_id: user._id 'metadata.user_id': user._id
}, {
fields: {
data: false
}
}); });
if (file === null) { if (file === null) {
@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'name' parameter // Get 'name' parameter
const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
if (nameErr) return rej('invalid name param'); if (nameErr) return rej('invalid name param');
if (name) file.name = name; if (name) file.filename = name;
// Get 'folder_id' parameter // Get 'folder_id' parameter
const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
@ -46,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (folderId !== undefined) { if (folderId !== undefined) {
if (folderId === null) { if (folderId === null) {
file.folder_id = null; file.metadata.folder_id = null;
} else { } else {
// Fetch folder // Fetch folder
const folder = await DriveFolder const folder = await DriveFolder
@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
return rej('folder-not-found'); return rej('folder-not-found');
} }
file.folder_id = folder._id; file.metadata.folder_id = folder._id;
} }
} }
DriveFile.update(file._id, { await DriveFile.update(file._id, {
$set: { $set: {
name: file.name, filename: file.filename,
folder_id: file.folder_id 'metadata.folder_id': file.metadata.folder_id
} }
}); });
@ -76,6 +72,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Response // Response
res(fileObj); res(fileObj);
// Publish drive_file_updated event // Publish file_updated event
event(user._id, 'drive_file_updated', fileObj); publishDriveStream(user._id, 'file_updated', fileObj);
}); });

View File

@ -2,11 +2,16 @@
* Module dependencies * Module dependencies
*/ */
import * as URL from 'url'; import * as URL from 'url';
const download = require('download');
import $ from 'cafy'; import $ from 'cafy';
import { validateFileName } from '../../../models/drive-file'; import { validateFileName } from '../../../models/drive-file';
import serialize from '../../../serializers/drive-file'; import serialize from '../../../serializers/drive-file';
import create from '../../../common/add-file-to-drive'; import create from '../../../common/add-file-to-drive';
import * as debug from 'debug';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as request from 'request';
const log = debug('misskey:endpoint:upload_from_url');
/** /**
* Create a file from a URL * Create a file from a URL
@ -15,11 +20,11 @@ import create from '../../../common/add-file-to-drive';
* @param {any} user * @param {any} user
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user) => new Promise(async (res, rej) => { module.exports = async (params, user): Promise<any> => {
// Get 'url' parameter // Get 'url' parameter
// TODO: Validate this url // TODO: Validate this url
const [url, urlErr] = $(params.url).string().$; const [url, urlErr] = $(params.url).string().$;
if (urlErr) return rej('invalid url param'); if (urlErr) throw 'invalid url param';
let name = URL.parse(url).pathname.split('/').pop(); let name = URL.parse(url).pathname.split('/').pop();
if (!validateFileName(name)) { if (!validateFileName(name)) {
@ -28,17 +33,35 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'folder_id' parameter // Get 'folder_id' parameter
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
if (folderIdErr) return rej('invalid folder_id param'); if (folderIdErr) throw 'invalid folder_id param';
// Download file // Create temp file
const data = await download(url); const path = await new Promise((res: (string) => void, rej) => {
tmp.file((e, path) => {
if (e) return rej(e);
res(path);
});
});
// Create file // write content at URL to temp file
const driveFile = await create(user, data, name, null, folderId); await new Promise((res, rej) => {
const writable = fs.createWriteStream(path);
request(url)
.on('error', rej)
.on('end', () => {
writable.close();
res(path);
})
.pipe(writable)
.on('error', rej);
});
// Serialize const driveFile = await create(user, path, name, null, folderId);
const fileObj = await serialize(driveFile);
// Response // clean-up
res(fileObj); fs.unlink(path, (e) => {
}); if (e) log(e.stack);
});
return serialize(driveFile);
};

View File

@ -5,7 +5,7 @@ import $ from 'cafy';
import DriveFolder from '../../../models/drive-folder'; import DriveFolder from '../../../models/drive-folder';
import { isValidFolderName } from '../../../models/drive-folder'; import { isValidFolderName } from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-folder'; import serialize from '../../../serializers/drive-folder';
import event from '../../../event'; import { publishDriveStream } from '../../../event';
/** /**
* Create drive folder * Create drive folder
@ -52,6 +52,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Response // Response
res(folderObj); res(folderObj);
// Publish drive_folder_created event // Publish folder_created event
event(user._id, 'drive_folder_created', folderObj); publishDriveStream(user._id, 'folder_created', folderObj);
}); });

View File

@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}); });
// Serialize // Serialize
res(await Promise.all(folders.map(async folder => res(await Promise.all(folders.map(folder => serialize(folder))));
await serialize(folder))));
}); });

View File

@ -4,8 +4,8 @@
import $ from 'cafy'; import $ from 'cafy';
import DriveFolder from '../../../models/drive-folder'; import DriveFolder from '../../../models/drive-folder';
import { isValidFolderName } from '../../../models/drive-folder'; import { isValidFolderName } from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-file'; import serialize from '../../../serializers/drive-folder';
import event from '../../../event'; import { publishDriveStream } from '../../../event';
/** /**
* Update a folder * Update a folder
@ -96,6 +96,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Response // Response
res(folderObj); res(folderObj);
// Publish drive_folder_updated event // Publish folder_updated event
event(user._id, 'drive_folder_updated', folderObj); publishDriveStream(user._id, 'folder_updated', folderObj);
}); });

View File

@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
_id: -1 _id: -1
}; };
const query = { const query = {
user_id: user._id 'metadata.user_id': user._id
} as any; } as any;
if (sinceId) { if (sinceId) {
sort._id = 1; sort._id = 1;
@ -52,15 +52,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}; };
} }
if (type) { if (type) {
query.type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
} }
// Issue query // Issue query
const files = await DriveFile const files = await DriveFile
.find(query, { .find(query, {
fields: {
data: false
},
limit: limit, limit: limit,
sort: sort sort: sort
}); });

View File

@ -13,38 +13,27 @@ import Appdata from '../../../models/appdata';
* @param {Boolean} isSecure * @param {Boolean} isSecure
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { module.exports = (params, user, app) => new Promise(async (res, rej) => {
if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
// Get 'key' parameter // Get 'key' parameter
const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$;
if (keyError) return rej('invalid key param'); if (keyError) return rej('invalid key param');
if (isSecure) { const select = {};
if (!user.data) { if (key !== null) {
return res(); select[`data.${key}`] = true;
} }
if (key !== null) { const appdata = await Appdata.findOne({
const data = {}; app_id: app._id,
data[key] = user.data[key]; user_id: user._id
res(data); }, {
} else { fields: select
res(user.data); });
}
} else {
const select = {};
if (key !== null) {
select[`data.${key}`] = true;
}
const appdata = await Appdata.findOne({
app_id: app._id,
user_id: user._id
}, {
fields: select
});
if (appdata) { if (appdata) {
res(appdata.data); res(appdata.data);
} else { } else {
res(); res();
}
} }
}); });

View File

@ -3,9 +3,6 @@
*/ */
import $ from 'cafy'; import $ from 'cafy';
import Appdata from '../../../models/appdata'; import Appdata from '../../../models/appdata';
import User from '../../../models/user';
import serialize from '../../../serializers/user';
import event from '../../../event';
/** /**
* Set app data * Set app data
@ -16,7 +13,9 @@ import event from '../../../event';
* @param {Boolean} isSecure * @param {Boolean} isSecure
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { module.exports = (params, user, app) => new Promise(async (res, rej) => {
if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
// Get 'data' parameter // Get 'data' parameter
const [data, dataError] = $(params.data).optional.object() const [data, dataError] = $(params.data).optional.object()
.pipe(obj => { .pipe(obj => {
@ -43,31 +42,17 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) =
set[`data.${key}`] = value; set[`data.${key}`] = value;
} }
if (isSecure) { await Appdata.update({
const _user = await User.findOneAndUpdate(user._id, { app_id: app._id,
user_id: user._id
}, Object.assign({
app_id: app._id,
user_id: user._id
}, {
$set: set $set: set
}), {
upsert: true
}); });
res(204); res(204);
// Publish i updated event
event(user._id, 'i_updated', await serialize(_user, user, {
detail: true,
includeSecrets: true
}));
} else {
await Appdata.update({
app_id: app._id,
user_id: user._id
}, Object.assign({
app_id: app._id,
user_id: user._id
}, {
$set: set
}), {
upsert: true
});
res(204);
}
}); });

View File

@ -22,15 +22,15 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
if (newPasswordErr) return rej('invalid new_password param'); if (newPasswordErr) return rej('invalid new_password param');
// Compare password // Compare password
const same = bcrypt.compareSync(currentPassword, user.password); const same = await bcrypt.compare(currentPassword, user.password);
if (!same) { if (!same) {
return rej('incorrect password'); return rej('incorrect password');
} }
// Generate hash of password // Generate hash of password
const salt = bcrypt.genSaltSync(8); const salt = await bcrypt.genSalt(8);
const hash = bcrypt.hashSync(newPassword, salt); const hash = await bcrypt.hash(newPassword, salt);
await User.update(user._id, { await User.update(user._id, {
$set: { $set: {

View File

@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
if (passwordErr) return rej('invalid password param'); if (passwordErr) return rej('invalid password param');
// Compare password // Compare password
const same = bcrypt.compareSync(password, user.password); const same = await bcrypt.compare(password, user.password);
if (!same) { if (!same) {
return rej('incorrect password'); return rej('incorrect password');

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 (bannerIdErr) return rej('invalid banner_id param');
if (bannerId) user.banner_id = bannerId; if (bannerId) user.banner_id = bannerId;
// Get 'show_donation' parameter
const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$;
if (showDonationErr) return rej('invalid show_donation param');
if (showDonation) user.client_settings.show_donation = showDonation;
await User.update(user._id, { await User.update(user._id, {
$set: { $set: {
name: user.name, name: user.name,
description: user.description, description: user.description,
avatar_id: user.avatar_id, avatar_id: user.avatar_id,
banner_id: user.banner_id, banner_id: user.banner_id,
profile: user.profile profile: user.profile,
'client_settings.show_donation': user.client_settings.show_donation
} }
}); });

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 DriveFile from '../../../models/drive-file';
import serialize from '../../../serializers/messaging-message'; import serialize from '../../../serializers/messaging-message';
import publishUserStream from '../../../event'; import publishUserStream from '../../../event';
import { publishMessagingStream } from '../../../event'; import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
import config from '../../../../conf'; import config from '../../../../conf';
/** /**
@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (fileId !== undefined) { if (fileId !== undefined) {
file = await DriveFile.findOne({ file = await DriveFile.findOne({
_id: fileId, _id: fileId,
user_id: user._id 'metadata.user_id': user._id
}, {
data: false
}); });
if (file === null) { if (file === null) {
@ -87,10 +85,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// 自分のストリーム // 自分のストリーム
publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
publishMessagingIndexStream(message.user_id, 'message', messageObj);
publishUserStream(message.user_id, 'messaging_message', messageObj); publishUserStream(message.user_id, 'messaging_message', messageObj);
// 相手のストリーム // 相手のストリーム
publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
publishMessagingIndexStream(message.recipient_id, 'message', messageObj);
publishUserStream(message.recipient_id, 'messaging_message', messageObj); publishUserStream(message.recipient_id, 'messaging_message', messageObj);
// 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
@ -98,6 +98,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
if (!freshMessage.is_read) { if (!freshMessage.is_read) {
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
} }
}, 3000); }, 3000);

View File

@ -4,6 +4,7 @@
import * as os from 'os'; import * as os from 'os';
import version from '../../version'; import version from '../../version';
import config from '../../conf'; import config from '../../conf';
import Meta from '../models/meta';
/** /**
* @swagger * @swagger
@ -39,6 +40,8 @@ import config from '../../conf';
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params) => new Promise(async (res, rej) => { module.exports = (params) => new Promise(async (res, rej) => {
const meta = (await Meta.findOne()) || {};
res({ res({
maintainer: config.maintainer, maintainer: config.maintainer,
version: version, version: version,
@ -49,6 +52,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
cpu: { cpu: {
model: os.cpus()[0].model, model: os.cpus()[0].model,
cores: os.cpus().length cores: os.cpus().length
} },
top_image: meta.top_image,
broadcasts: meta.broadcasts
}); });
}); });

View File

@ -14,7 +14,7 @@ import ChannelWatching from '../../models/channel-watching';
import serialize from '../../serializers/post'; import serialize from '../../serializers/post';
import notify from '../../common/notify'; import notify from '../../common/notify';
import watch from '../../common/watch-post'; import watch from '../../common/watch-post';
import { default as event, publishChannelStream } from '../../event'; import event, { pushSw, publishChannelStream } from '../../event';
import config from '../../../conf'; import config from '../../../conf';
/** /**
@ -44,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
// SELECT _id // SELECT _id
const entity = await DriveFile.findOne({ const entity = await DriveFile.findOne({
_id: mediaId, _id: mediaId,
user_id: user._id 'metadata.user_id': user._id
}, {
_id: true
}); });
if (entity === null) { if (entity === null) {
@ -236,7 +234,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
const mentions = []; const mentions = [];
function addMention(mentionee, type) { function addMention(mentionee, reason) {
// Reject if already added // Reject if already added
if (mentions.some(x => x.equals(mentionee))) return; if (mentions.some(x => x.equals(mentionee))) return;
@ -245,7 +243,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
// Publish event // Publish event
if (!user._id.equals(mentionee)) { if (!user._id.equals(mentionee)) {
event(mentionee, type, postObj); event(mentionee, reason, postObj);
pushSw(mentionee, reason, postObj);
} }
} }

View File

@ -7,7 +7,9 @@ import Post from '../../../models/post';
import Watching from '../../../models/post-watching'; import Watching from '../../../models/post-watching';
import notify from '../../../common/notify'; import notify from '../../../common/notify';
import watch from '../../../common/watch-post'; import watch from '../../../common/watch-post';
import { publishPostStream } from '../../../event'; import { publishPostStream, pushSw } from '../../../event';
import serializePost from '../../../serializers/post';
import serializeUser from '../../../serializers/user';
/** /**
* React to a post * React to a post
@ -87,6 +89,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
reaction: reaction reaction: reaction
}); });
pushSw(post.user_id, 'reaction', {
user: await serializeUser(user, post.user_id),
post: await serializePost(post, post.user_id),
reaction: reaction
});
// Fetch watchers // Fetch watchers
Watching Watching
.find({ .find({

View File

@ -2,6 +2,7 @@
* Module dependencies * Module dependencies
*/ */
import $ from 'cafy'; import $ from 'cafy';
import rap from '@prezzemolo/rap';
import Post from '../../models/post'; import Post from '../../models/post';
import ChannelWatching from '../../models/channel-watching'; import ChannelWatching from '../../models/channel-watching';
import getFriends from '../../common/get-friends'; import getFriends from '../../common/get-friends';
@ -15,32 +16,41 @@ import serialize from '../../serializers/post';
* @param {any} app * @param {any} app
* @return {Promise<any>} * @return {Promise<any>}
*/ */
module.exports = (params, user, app) => new Promise(async (res, rej) => { module.exports = async (params, user, app) => {
// Get 'limit' parameter // Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param'); if (limitErr) throw 'invalid limit param';
// Get 'since_id' parameter // Get 'since_id' parameter
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param'); if (sinceIdErr) throw 'invalid since_id param';
// Get 'max_id' parameter // Get 'max_id' parameter
const [maxId, maxIdErr] = $(params.max_id).optional.id().$; const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
if (maxIdErr) return rej('invalid max_id param'); if (maxIdErr) throw 'invalid max_id param';
// Check if both of since_id and max_id is specified // Get 'since_date' parameter
if (sinceId && maxId) { const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
return rej('cannot set since_id and max_id'); if (sinceDateErr) throw 'invalid since_date param';
// Get 'max_date' parameter
const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
if (maxDateErr) throw 'invalid max_date param';
// Check if only one of since_id, max_id, since_date, max_date specified
if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
throw 'only one of since_id, max_id, since_date, max_date can be specified';
} }
// ID list of the user itself and other users who the user follows const { followingIds, watchingChannelIds } = await rap({
const followingIds = await getFriends(user._id); // ID list of the user itself and other users who the user follows
followingIds: getFriends(user._id),
// Watchしているチャンネルを取得 // Watchしているチャンネルを取得
const watches = await ChannelWatching.find({ watchingChannelIds: ChannelWatching.find({
user_id: user._id, user_id: user._id,
// 削除されたドキュメントは除く // 削除されたドキュメントは除く
deleted_at: { $exists: false } deleted_at: { $exists: false }
}).then(watches => watches.map(w => w.channel_id))
}); });
//#region Construct query //#region Construct query
@ -65,7 +75,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
}, { }, {
// Watchしているチャンネルへの投稿 // Watchしているチャンネルへの投稿
channel_id: { channel_id: {
$in: watches.map(w => w.channel_id) $in: watchingChannelIds
} }
}] }]
} as any; } as any;
@ -79,6 +89,15 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
query._id = { query._id = {
$lt: maxId $lt: maxId
}; };
} else if (sinceDate) {
sort._id = 1;
query.created_at = {
$gt: new Date(sinceDate)
};
} else if (maxDate) {
query.created_at = {
$lt: new Date(maxDate)
};
} }
//#endregion //#endregion
@ -90,7 +109,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
}); });
// Serialize // Serialize
res(await Promise.all(timeline.map(async post => return await Promise.all(timeline.map(post => serialize(post, user)));
await serialize(post, user) };
)));
});

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().$; const [userId, userIdErr] = $(params.user_id).id().$;
if (userIdErr) return rej('invalid user_id param'); if (userIdErr) return rej('invalid user_id param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Lookup user // Lookup user
const user = await User.findOne({ const user = await User.findOne({
_id: userId _id: userId
@ -82,8 +86,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Sort replies by frequency // Sort replies by frequency
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Lookup top 10 replies // Extract top replied users
const topRepliedUsers = repliedUsersSorted.slice(0, 10); const topRepliedUsers = repliedUsersSorted.slice(0, limit);
// Make replies object (includes weights) // Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({

View File

@ -46,9 +46,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
const [maxId, maxIdErr] = $(params.max_id).optional.id().$; const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
if (maxIdErr) return rej('invalid max_id param'); if (maxIdErr) return rej('invalid max_id param');
// Check if both of since_id and max_id is specified // Get 'since_date' parameter
if (sinceId && maxId) { const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
return rej('cannot set since_id and max_id'); if (sinceDateErr) throw 'invalid since_date param';
// Get 'max_date' parameter
const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
if (maxDateErr) throw 'invalid max_date param';
// Check if only one of since_id, max_id, since_date, max_date specified
if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
throw 'only one of since_id, max_id, since_date, max_date can be specified';
} }
const q = userId !== undefined const q = userId !== undefined
@ -66,13 +74,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
return rej('user not found'); return rej('user not found');
} }
// Construct query //#region Construct query
const sort = { const sort = {
_id: -1 _id: -1
}; };
const query = { const query = {
user_id: user._id user_id: user._id
} as any; } as any;
if (sinceId) { if (sinceId) {
sort._id = 1; sort._id = 1;
query._id = { query._id = {
@ -82,6 +92,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
query._id = { query._id = {
$lt: maxId $lt: maxId
}; };
} else if (sinceDate) {
sort._id = 1;
query.created_at = {
$gt: new Date(sinceDate)
};
} else if (maxDate) {
query.created_at = {
$lt: new Date(maxDate)
};
} }
if (!includeReplies) { if (!includeReplies) {
@ -94,6 +113,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
$ne: null $ne: null
}; };
} }
//#endregion
// Issue query // Issue query
const posts = await Post const posts = await Post

View File

@ -1,5 +1,6 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import * as redis from 'redis'; import * as redis from 'redis';
import swPush from './common/push-sw';
import config from '../conf'; import config from '../conf';
type ID = string | mongo.ObjectID; type ID = string | mongo.ObjectID;
@ -17,6 +18,14 @@ class MisskeyEvent {
this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
} }
public publishSw(userId: ID, type: string, value?: any): void {
swPush(userId, type, value);
}
public publishDriveStream(userId: ID, type: string, value?: any): void {
this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishPostStream(postId: ID, type: string, value?: any): void { public publishPostStream(postId: ID, type: string, value?: any): void {
this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value); this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value);
} }
@ -25,6 +34,10 @@ class MisskeyEvent {
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
} }
public publishMessagingIndexStream(userId: ID, type: string, value?: any): void {
this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishChannelStream(channelId: ID, type: string, value?: any): void { public publishChannelStream(channelId: ID, type: string, value?: any): void {
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value); this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
} }
@ -42,8 +55,14 @@ const ev = new MisskeyEvent();
export default ev.publishUserStream.bind(ev); export default ev.publishUserStream.bind(ev);
export const pushSw = ev.publishSw.bind(ev);
export const publishDriveStream = ev.publishDriveStream.bind(ev);
export const publishPostStream = ev.publishPostStream.bind(ev); export const publishPostStream = ev.publishPostStream.bind(ev);
export const publishMessagingStream = ev.publishMessagingStream.bind(ev); export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
export const publishChannelStream = ev.publishChannelStream.bind(ev); export const publishChannelStream = ev.publishChannelStream.bind(ev);

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'); const collection = monkDb.get('drive_files.files');
(collection as any).createIndex('hash'); // fuck type definition
export default collection as any; // fuck type definition export default collection as any; // fuck type definition
const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongodb.GridFSBucket(db, {
bucketName: 'drive_files'
});
return bucket;
};
export { getGridFSBucket };
export function validateFileName(name: string): boolean { export function validateFileName(name: string): boolean {
return ( return (
(name.trim().length > 0) && (name.trim().length > 0) &&

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 * as mongo from 'mongodb';
import db from '../../db/mongodb'; import db from '../../db/mongodb';
import { IUser } from './user';
export default db.get('notifications') as any; // fuck type definition export default db.get('notifications') as any; // fuck type definition
export interface INotification { export interface INotification {
_id: mongo.ObjectID; _id: mongo.ObjectID;
created_at: Date;
/**
*
*/
notifiee?: IUser;
/**
*
*/
notifiee_id: mongo.ObjectID;
/**
* (initiator)Origin
*/
notifier?: IUser;
/**
* (initiator)Origin
*/
notifier_id: mongo.ObjectID;
/**
*
* follow -
* mention - 稿
* reply - (Watchしている)稿
* repost - (Watchしている)稿Repostされた
* quote - (Watchしている)稿Repostされた
* reaction - (Watchしている)稿
* poll_vote - (Watchしている)稿
*/
type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote';
/**
*
*/
is_read: Boolean;
} }

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 Signin from '../models/signin';
import serialize from '../serializers/signin'; import serialize from '../serializers/signin';
import event from '../event'; import event from '../event';
import config from '../../conf'; import signin from '../common/signin';
export default async (req: express.Request, res: express.Response) => { export default async (req: express.Request, res: express.Response) => {
res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Credentials', 'true');
@ -40,20 +40,10 @@ export default async (req: express.Request, res: express.Response) => {
} }
// Compare password // Compare password
const same = bcrypt.compareSync(password, user.password); const same = await bcrypt.compare(password, user.password);
if (same) { if (same) {
const expires = 1000 * 60 * 60 * 24 * 365; // One Year signin(res, user, false);
res.cookie('i', user.token, {
path: '/',
domain: `.${config.host}`,
secure: config.url.substr(0, 5) === 'https',
httpOnly: false,
expires: new Date(Date.now() + expires),
maxAge: expires
});
res.sendStatus(204);
} else { } else {
res.status(400).send({ res.status(400).send({
error: 'incorrect password' error: 'incorrect password'

View File

@ -1,3 +1,4 @@
import * as uuid from 'uuid';
import * as express from 'express'; import * as express from 'express';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
import recaptcha = require('recaptcha-promise'); import recaptcha = require('recaptcha-promise');
@ -8,9 +9,31 @@ import generateUserToken from '../common/generate-native-user-token';
import config from '../../conf'; import config from '../../conf';
recaptcha.init({ recaptcha.init({
secret_key: config.recaptcha.secretKey secret_key: config.recaptcha.secret_key
}); });
const home = {
left: [
'profile',
'calendar',
'activity',
'rss-reader',
'trends',
'photo-stream',
'version'
],
right: [
'broadcast',
'notifications',
'user-recommendation',
'recommended-polls',
'server',
'donation',
'nav',
'tips'
]
};
export default async (req: express.Request, res: express.Response) => { export default async (req: express.Request, res: express.Response) => {
// Verify recaptcha // Verify recaptcha
// ただしテスト時はこの機構は障害となるため無効にする // ただしテスト時はこの機構は障害となるため無効にする
@ -54,12 +77,34 @@ export default async (req: express.Request, res: express.Response) => {
} }
// Generate hash of password // Generate hash of password
const salt = bcrypt.genSaltSync(8); const salt = await bcrypt.genSalt(8);
const hash = bcrypt.hashSync(password, salt); const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateUserToken(); const secret = generateUserToken();
//#region Construct home data
const homeData = [];
home.left.forEach(widget => {
homeData.push({
name: widget,
id: uuid(),
place: 'left',
data: {}
});
});
home.right.forEach(widget => {
homeData.push({
name: widget,
id: uuid(),
place: 'right',
data: {}
});
});
//#endregion
// Create account // Create account
const account: IUser = await User.insert({ const account: IUser = await User.insert({
token: secret, token: secret,
@ -88,6 +133,11 @@ export default async (req: express.Request, res: express.Response) => {
height: null, height: null,
location: null, location: null,
weight: null weight: null
},
settings: {},
client_settings: {
home: homeData,
show_donation: false
} }
}); });

View File

@ -31,44 +31,44 @@ export default (
if (mongo.ObjectID.prototype.isPrototypeOf(file)) { if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
_file = await DriveFile.findOne({ _file = await DriveFile.findOne({
_id: file _id: file
}, { });
fields: {
data: false
}
});
} else if (typeof file === 'string') { } else if (typeof file === 'string') {
_file = await DriveFile.findOne({ _file = await DriveFile.findOne({
_id: new mongo.ObjectID(file) _id: new mongo.ObjectID(file)
}, { });
fields: {
data: false
}
});
} else { } else {
_file = deepcopy(file); _file = deepcopy(file);
} }
// Rename _id to id if (!_file) return reject('invalid file arg.');
_file.id = _file._id;
delete _file._id;
delete _file.data; // rendered target
let _target: any = {};
_file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`; _target.id = _file._id;
_target.created_at = _file.uploadDate;
_target.name = _file.filename;
_target.type = _file.contentType;
_target.datasize = _file.length;
_target.md5 = _file.md5;
if (opts.detail && _file.folder_id) { _target = Object.assign(_target, _file.metadata);
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
if (opts.detail && _target.folder_id) {
// Populate folder // Populate folder
_file.folder = await serializeDriveFolder(_file.folder_id, { _target.folder = await serializeDriveFolder(_target.folder_id, {
detail: true detail: true
}); });
} }
if (opts.detail && _file.tags) { if (opts.detail && _target.tags) {
// Populate tags // Populate tags
_file.tags = await _file.tags.map(async (tag: any) => _target.tags = await _target.tags.map(async (tag: any) =>
await serializeDriveTag(tag) await serializeDriveTag(tag)
); );
} }
resolve(_file); resolve(_target);
}); });

View File

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

View File

@ -12,6 +12,7 @@ import serializeChannel from './channel';
import serializeUser from './user'; import serializeUser from './user';
import serializeDriveFile from './drive-file'; import serializeDriveFile from './drive-file';
import parse from '../common/text'; import parse from '../common/text';
import rap from '@prezzemolo/rap';
/** /**
* Serialize a post * Serialize a post
@ -21,13 +22,13 @@ import parse from '../common/text';
* @param options? serialize options * @param options? serialize options
* @return response * @return response
*/ */
const self = ( const self = async (
post: string | mongo.ObjectID | IPost, post: string | mongo.ObjectID | IPost,
me?: string | mongo.ObjectID | IUser, me?: string | mongo.ObjectID | IUser,
options?: { options?: {
detail: boolean detail: boolean
} }
) => new Promise<any>(async (resolve, reject) => { ) => {
const opts = options || { const opts = options || {
detail: true, detail: true,
}; };
@ -56,6 +57,8 @@ const self = (
_post = deepcopy(post); _post = deepcopy(post);
} }
if (!_post) throw 'invalid post arg.';
const id = _post._id; const id = _post._id;
// Rename _id to id // Rename _id to id
@ -70,105 +73,120 @@ const self = (
} }
// Populate user // Populate user
_post.user = await serializeUser(_post.user_id, meId); _post.user = serializeUser(_post.user_id, meId);
// Populate app // Populate app
if (_post.app_id) { if (_post.app_id) {
_post.app = await serializeApp(_post.app_id); _post.app = serializeApp(_post.app_id);
} }
// Populate channel // Populate channel
if (_post.channel_id) { if (_post.channel_id) {
_post.channel = await serializeChannel(_post.channel_id); _post.channel = serializeChannel(_post.channel_id);
} }
// Populate media // Populate media
if (_post.media_ids) { if (_post.media_ids) {
_post.media = await Promise.all(_post.media_ids.map(async fileId => _post.media = Promise.all(_post.media_ids.map(fileId =>
await serializeDriveFile(fileId) serializeDriveFile(fileId)
)); ));
} }
// When requested a detailed post data // When requested a detailed post data
if (opts.detail) { if (opts.detail) {
// Get previous post info // Get previous post info
const prev = await Post.findOne({ _post.prev = (async () => {
user_id: _post.user_id, const prev = await Post.findOne({
_id: { user_id: _post.user_id,
$lt: id _id: {
} $lt: id
}, { }
fields: { }, {
_id: true fields: {
}, _id: true
sort: { },
_id: -1 sort: {
} _id: -1
}); }
_post.prev = prev ? prev._id : null; });
return prev ? prev._id : null;
})();
// Get next post info // Get next post info
const next = await Post.findOne({ _post.next = (async () => {
user_id: _post.user_id, const next = await Post.findOne({
_id: { user_id: _post.user_id,
$gt: id _id: {
} $gt: id
}, { }
fields: { }, {
_id: true fields: {
}, _id: true
sort: { },
_id: 1 sort: {
} _id: 1
}); }
_post.next = next ? next._id : null; });
return next ? next._id : null;
})();
if (_post.reply_id) { if (_post.reply_id) {
// Populate reply to post // Populate reply to post
_post.reply = await self(_post.reply_id, meId, { _post.reply = self(_post.reply_id, meId, {
detail: false detail: false
}); });
} }
if (_post.repost_id) { if (_post.repost_id) {
// Populate repost // Populate repost
_post.repost = await self(_post.repost_id, meId, { _post.repost = self(_post.repost_id, meId, {
detail: _post.text == null detail: _post.text == null
}); });
} }
// Poll // Poll
if (meId && _post.poll) { if (meId && _post.poll) {
const vote = await Vote _post.poll = (async (poll) => {
.findOne({ const vote = await Vote
user_id: meId, .findOne({
post_id: id user_id: meId,
}); post_id: id
});
if (vote != null) { if (vote != null) {
const myChoice = _post.poll.choices const myChoice = poll.choices
.filter(c => c.id == vote.choice)[0]; .filter(c => c.id == vote.choice)[0];
myChoice.is_voted = true; myChoice.is_voted = true;
} }
return poll;
})(_post.poll);
} }
// Fetch my reaction // Fetch my reaction
if (meId) { if (meId) {
const reaction = await Reaction _post.my_reaction = (async () => {
.findOne({ const reaction = await Reaction
user_id: meId, .findOne({
post_id: id, user_id: meId,
deleted_at: { $exists: false } post_id: id,
}); deleted_at: { $exists: false }
});
if (reaction) { if (reaction) {
_post.my_reaction = reaction.reaction; return reaction.reaction;
} }
return null;
})();
} }
} }
resolve(_post); // resolve promises in _post object
}); _post = await rap(_post);
return _post;
};
export default self; export default self;

View File

@ -8,6 +8,7 @@ import serializePost from './post';
import Following from '../models/following'; import Following from '../models/following';
import getFriends from '../common/get-friends'; import getFriends from '../common/get-friends';
import config from '../../conf'; import config from '../../conf';
import rap from '@prezzemolo/rap';
/** /**
* Serialize a user * Serialize a user
@ -34,9 +35,10 @@ export default (
let _user: any; let _user: any;
const fields = opts.detail ? { const fields = opts.detail ? {
data: false settings: false
} : { } : {
data: false, settings: false,
client_settings: false,
profile: false, profile: false,
keywords: false, keywords: false,
domains: false domains: false
@ -55,6 +57,8 @@ export default (
_user = deepcopy(user); _user = deepcopy(user);
} }
if (!_user) return reject('invalid user arg.');
// Me // Me
const meId: mongo.ObjectID = me const meId: mongo.ObjectID = me
? mongo.ObjectID.prototype.isPrototypeOf(me) ? mongo.ObjectID.prototype.isPrototypeOf(me)
@ -69,7 +73,7 @@ export default (
delete _user._id; delete _user._id;
// Remove needless properties // Remove needless properties
delete _user.lates_post; delete _user.latest_post;
// Remove private properties // Remove private properties
delete _user.password; delete _user.password;
@ -83,8 +87,8 @@ export default (
// Visible via only the official client // Visible via only the official client
if (!opts.includeSecrets) { if (!opts.includeSecrets) {
delete _user.data;
delete _user.email; delete _user.email;
delete _user.client_settings;
} }
_user.avatar_url = _user.avatar_id != null _user.avatar_url = _user.avatar_id != null
@ -104,26 +108,30 @@ export default (
if (meId && !meId.equals(_user.id)) { if (meId && !meId.equals(_user.id)) {
// If the user is following // If the user is following
const follow = await Following.findOne({ _user.is_following = (async () => {
follower_id: meId, const follow = await Following.findOne({
followee_id: _user.id, follower_id: meId,
deleted_at: { $exists: false } followee_id: _user.id,
}); deleted_at: { $exists: false }
_user.is_following = follow !== null; });
return follow !== null;
})();
// If the user is followed // If the user is followed
const follow2 = await Following.findOne({ _user.is_followed = (async () => {
follower_id: _user.id, const follow2 = await Following.findOne({
followee_id: meId, follower_id: _user.id,
deleted_at: { $exists: false } followee_id: meId,
}); deleted_at: { $exists: false }
_user.is_followed = follow2 !== null; });
return follow2 !== null;
})();
} }
if (opts.detail) { if (opts.detail) {
if (_user.pinned_post_id) { if (_user.pinned_post_id) {
// Populate pinned post // Populate pinned post
_user.pinned_post = await serializePost(_user.pinned_post_id, meId, { _user.pinned_post = serializePost(_user.pinned_post_id, meId, {
detail: true detail: true
}); });
} }
@ -132,23 +140,24 @@ export default (
const myFollowingIds = await getFriends(meId); const myFollowingIds = await getFriends(meId);
// Get following you know count // Get following you know count
const followingYouKnowCount = await Following.count({ _user.following_you_know_count = Following.count({
followee_id: { $in: myFollowingIds }, followee_id: { $in: myFollowingIds },
follower_id: _user.id, follower_id: _user.id,
deleted_at: { $exists: false } deleted_at: { $exists: false }
}); });
_user.following_you_know_count = followingYouKnowCount;
// Get followers you know count // Get followers you know count
const followersYouKnowCount = await Following.count({ _user.followers_you_know_count = Following.count({
followee_id: _user.id, followee_id: _user.id,
follower_id: { $in: myFollowingIds }, follower_id: { $in: myFollowingIds },
deleted_at: { $exists: false } deleted_at: { $exists: false }
}); });
_user.followers_you_know_count = followersYouKnowCount;
} }
} }
// resolve promises in _user object
_user = await rap(_user);
resolve(_user); resolve(_user);
}); });
/* /*

View File

@ -40,7 +40,7 @@ app.get('/', (req, res) => {
endpoints.forEach(endpoint => endpoints.forEach(endpoint =>
endpoint.withFile ? endpoint.withFile ?
app.post(`/${endpoint.name}`, app.post(`/${endpoint.name}`,
endpoint.withFile ? multer({ dest: 'uploads/' }).single('file') : null, endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null,
require('./api-handler').default.bind(null, endpoint)) : require('./api-handler').default.bind(null, endpoint)) :
app.post(`/${endpoint.name}`, app.post(`/${endpoint.name}`,
require('./api-handler').default.bind(null, endpoint)) require('./api-handler').default.bind(null, endpoint))

View File

@ -1,4 +1,6 @@
import * as express from 'express'; import * as express from 'express';
import * as cookie from 'cookie';
import * as uuid from 'uuid';
// import * as Twitter from 'twitter'; // import * as Twitter from 'twitter';
// const Twitter = require('twitter'); // const Twitter = require('twitter');
import autwh from 'autwh'; import autwh from 'autwh';
@ -7,6 +9,7 @@ import User from '../models/user';
import serialize from '../serializers/user'; import serialize from '../serializers/user';
import event from '../event'; import event from '../event';
import config from '../../conf'; import config from '../../conf';
import signin from '../common/signin';
module.exports = (app: express.Application) => { module.exports = (app: express.Application) => {
app.get('/disconnect/twitter', async (req, res): Promise<any> => { app.get('/disconnect/twitter', async (req, res): Promise<any> => {
@ -30,8 +33,13 @@ module.exports = (app: express.Application) => {
if (config.twitter == null) { if (config.twitter == null) {
app.get('/connect/twitter', (req, res) => { app.get('/connect/twitter', (req, res) => {
res.send('現在Twitterへ接続できません'); res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
}); });
app.get('/signin/twitter', (req, res) => {
res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)');
});
return; return;
} }
@ -48,14 +56,58 @@ module.exports = (app: express.Application) => {
res.redirect(ctx.url); res.redirect(ctx.url);
}); });
app.get('/tw/cb', (req, res): any => { app.get('/signin/twitter', async (req, res): Promise<any> => {
if (res.locals.user == null) return res.send('plz signin'); const ctx = await twAuth.begin();
redis.get(res.locals.user, async (_, ctx) => {
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
const user = await User.findOneAndUpdate({ const sessid = uuid();
token: res.locals.user
}, { redis.set(sessid, JSON.stringify(ctx));
const expires = 1000 * 60 * 60; // 1h
res.cookie('signin_with_twitter_session_id', sessid, {
path: '/',
domain: `.${config.host}`,
secure: config.url.substr(0, 5) === 'https',
httpOnly: true,
expires: new Date(Date.now() + expires),
maxAge: expires
});
res.redirect(ctx.url);
});
app.get('/tw/cb', (req, res): any => {
if (res.locals.user == null) {
// req.headers['cookie'] は常に string ですが、型定義の都合上
// string | string[] になっているので string を明示しています
const cookies = cookie.parse((req.headers['cookie'] as string || ''));
const sessid = cookies['signin_with_twitter_session_id'];
if (sessid == undefined) {
res.status(400).send('invalid session');
}
redis.get(sessid, async (_, ctx) => {
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
const user = await User.findOne({
'twitter.user_id': result.userId
});
if (user == null) {
res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
}
signin(res, user, true);
});
} else {
redis.get(res.locals.user, async (_, ctx) => {
const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
const user = await User.findOneAndUpdate({
token: res.locals.user
}, {
$set: { $set: {
twitter: { twitter: {
access_token: result.accessToken, access_token: result.accessToken,
@ -66,13 +118,14 @@ module.exports = (app: express.Application) => {
} }
}); });
res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
// Publish i updated event // Publish i updated event
event(user._id, 'i_updated', await serialize(user, user, { event(user._id, 'i_updated', await serialize(user, user, {
detail: true, detail: true,
includeSecrets: true includeSecrets: true
})); }));
}); });
}
}); });
}; };

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 isNativeToken from './common/is-native-token';
import homeStream from './stream/home'; import homeStream from './stream/home';
import driveStream from './stream/drive';
import messagingStream from './stream/messaging'; import messagingStream from './stream/messaging';
import messagingIndexStream from './stream/messaging-index';
import serverStream from './stream/server'; import serverStream from './stream/server';
import requestsStream from './stream/requests';
import channelStream from './stream/channel'; import channelStream from './stream/channel';
module.exports = (server: http.Server) => { module.exports = (server: http.Server) => {
@ -27,6 +30,11 @@ module.exports = (server: http.Server) => {
return; return;
} }
if (request.resourceURL.pathname === '/requests') {
requestsStream(request, connection);
return;
}
// Connect to Redis // Connect to Redis
const subscriber = redis.createClient( const subscriber = redis.createClient(
config.redis.port, config.redis.host); config.redis.port, config.redis.host);
@ -51,7 +59,9 @@ module.exports = (server: http.Server) => {
const channel = const channel =
request.resourceURL.pathname === '/' ? homeStream : request.resourceURL.pathname === '/' ? homeStream :
request.resourceURL.pathname === '/drive' ? driveStream :
request.resourceURL.pathname === '/messaging' ? messagingStream : request.resourceURL.pathname === '/messaging' ? messagingStream :
request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
null; null;
if (channel !== null) { if (channel !== null) {

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 fs from 'fs';
import * as URL from 'url';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import isUrl = require('is-url'); import isUrl = require('is-url');
@ -23,16 +22,23 @@ export const path = process.env.NODE_ENV == 'test'
* *
*/ */
type Source = { type Source = {
maintainer: string; /**
*
*/
maintainer: {
/**
*
*/
name: string;
/**
* (URLかmailto形式のURL)
*/
url: string;
};
url: string; url: string;
secondary_url: string; secondary_url: string;
port: number; port: number;
https: { https?: { [x: string]: string };
enable: boolean;
key: string;
cert: string;
ca: string;
};
mongodb: { mongodb: {
host: string; host: string;
port: number; port: number;
@ -52,8 +58,8 @@ type Source = {
pass: string; pass: string;
}; };
recaptcha: { recaptcha: {
siteKey: string; site_key: string;
secretKey: string; secret_key: string;
}; };
accesslog?: string; accesslog?: string;
accesses?: { accesses?: {
@ -75,6 +81,14 @@ type Source = {
analysis?: { analysis?: {
mecab_command?: string; mecab_command?: string;
}; };
/**
* Service Worker
*/
sw?: {
public_key: string;
private_key: string;
};
}; };
/** /**
@ -106,14 +120,6 @@ export default function load() {
if (!isUrl(config.url)) urlError(config.url); if (!isUrl(config.url)) urlError(config.url);
if (!isUrl(config.secondary_url)) urlError(config.secondary_url); if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
const url = URL.parse(config.url);
const head = url.host.split('.')[0];
if (head != 'misskey') {
console.error(`プライマリドメインは、必ず「misskey」ドメインで始まっていなければなりません(現在の設定では「${head}」で始まっています)。例えば「https://misskey.xyz」「http://misskey.my.app.example.com」などが正しいプライマリURLです。`);
process.exit();
}
config.url = normalizeUrl(config.url); config.url = normalizeUrl(config.url);
config.secondary_url = normalizeUrl(config.secondary_url); config.secondary_url = normalizeUrl(config.secondary_url);

View File

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

View File

@ -1,11 +1,38 @@
import * as mongo from 'monk';
import config from '../conf'; import config from '../conf';
const uri = config.mongodb.user && config.mongodb.pass const uri = config.mongodb.user && config.mongodb.pass
? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` ? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; : `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
/**
* monk
*/
import * as mongo from 'monk';
const db = mongo(uri); const db = mongo(uri);
export default db; export default db;
/**
* MongoDB native module (officialy)
*/
import * as mongodb from 'mongodb';
let mdb: mongodb.Db;
const nativeDbConn = async (): Promise<mongodb.Db> => {
if (mdb) return mdb;
const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
mongodb.MongoClient.connect(uri, (e, db) => {
if (e) return reject(e);
resolve(db);
});
}))();
mdb = db;
return db;
};
export { nativeDbConn };

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -8,8 +8,9 @@ import * as bodyParser from 'body-parser';
import * as cors from 'cors'; import * as cors from 'cors';
import * as mongodb from 'mongodb'; import * as mongodb from 'mongodb';
import * as gm from 'gm'; import * as gm from 'gm';
import * as stream from 'stream';
import File from '../api/models/drive-file'; import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
/** /**
* Init app * Init app
@ -33,101 +34,127 @@ app.get('/', (req, res) => {
}); });
app.get('/default-avatar.jpg', (req, res) => { app.get('/default-avatar.jpg', (req, res) => {
const file = fs.readFileSync(`${__dirname}/assets/avatar.jpg`); const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`);
send(file, 'image/jpeg', req, res); send(file, 'image/jpeg', req, res);
}); });
app.get('/app-default.jpg', (req, res) => { app.get('/app-default.jpg', (req, res) => {
const file = fs.readFileSync(`${__dirname}/assets/dummy.png`); const file = fs.createReadStream(`${__dirname}/assets/dummy.png`);
send(file, 'image/png', req, res); send(file, 'image/png', req, res);
}); });
async function raw(data: Buffer, type: string, download: boolean, res: express.Response): Promise<any> { interface ISend {
res.header('Content-Type', type); contentType: string;
stream: stream.Readable;
if (download) {
res.header('Content-Disposition', 'attachment');
}
res.send(data);
} }
async function thumbnail(data: Buffer, type: string, resize: number, res: express.Response): Promise<any> { function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
if (!/^image\/.*$/.test(type)) { const readable: stream.Readable = (() => {
data = fs.readFileSync(`${__dirname}/assets/dummy.png`); // 画像ではない場合
} if (!/^image\/.*$/.test(type)) {
// 使わないことにしたストリームはしっかり取り壊しておく
data.destroy();
return fs.createReadStream(`${__dirname}/assets/not-an-image.png`);
}
let g = gm(data); const imageType = type.split('/')[1];
// 画像でもPNGかJPEGでないならダメ
if (imageType != 'png' && imageType != 'jpeg') {
// 使わないことにしたストリームはしっかり取り壊しておく
data.destroy();
return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
}
return data;
})();
let g = gm(readable);
if (resize) { if (resize) {
g = g.resize(resize, resize); g = g.resize(resize, resize);
} }
g const stream = g
.compress('jpeg') .compress('jpeg')
.quality(80) .quality(80)
.toBuffer('jpeg', (err, img) => { .stream();
if (err !== undefined && err !== null) {
console.error(err);
res.sendStatus(500);
return;
}
res.header('Content-Type', 'image/jpeg'); return {
res.send(img); contentType: 'image/jpeg',
}); stream
};
} }
function send(data: Buffer, type: string, req: express.Request, res: express.Response): void { const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => {
if (req.query.thumbnail !== undefined) { console.dir(e);
thumbnail(data, type, req.query.size, res); req.destroy();
} else { res.destroy(e);
raw(data, type, req.query.download !== undefined, res); };
function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void {
readable.on('error', commonReadableHandlerGenerator(req, res));
const data = ((): ISend => {
if (req.query.thumbnail !== undefined) {
return thumbnail(readable, type, req.query.size);
}
return {
contentType: type,
stream: readable
};
})();
if (readable !== data.stream) {
data.stream.on('error', commonReadableHandlerGenerator(req, res));
} }
if (req.query.download !== undefined) {
res.header('Content-Disposition', 'attachment');
}
res.header('Content-Type', data.contentType);
data.stream.pipe(res);
data.stream.on('end', () => {
res.end();
});
}
async function sendFileById(req: express.Request, res: express.Response): Promise<void> {
// Validate id
if (!mongodb.ObjectID.isValid(req.params.id)) {
res.status(400).send('incorrect id');
return;
}
const fileId = new mongodb.ObjectID(req.params.id);
const file = await DriveFile.findOne({ _id: fileId });
// validate name
if (req.params.name !== undefined && req.params.name !== file.filename) {
res.status(404).send('there is no file has given name');
return;
}
if (file == null) {
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
return;
}
const bucket = await getGridFSBucket();
const readable = bucket.openDownloadStream(fileId);
send(readable, file.contentType, req, res);
} }
/** /**
* Routing * Routing
*/ */
app.get('/:id', async (req, res) => { app.get('/:id', sendFileById);
// Validate id app.get('/:id/:name', sendFileById);
if (!mongodb.ObjectID.isValid(req.params.id)) {
res.status(400).send('incorrect id');
return;
}
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
if (file == null) {
res.status(404).sendFile(`${__dirname} / assets / dummy.png`);
return;
} else if (file.data == null) {
res.sendStatus(400);
return;
}
send(file.data.buffer, file.type, req, res);
});
app.get('/:id/:name', async (req, res) => {
// Validate id
if (!mongodb.ObjectID.isValid(req.params.id)) {
res.status(400).send('incorrect id');
return;
}
const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
if (file == null) {
res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
return;
} else if (file.data == null) {
res.sendStatus(400);
return;
}
send(file.data.buffer, file.type, req, res);
});
module.exports = app; module.exports = app;

View File

@ -8,7 +8,7 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as cluster from 'cluster'; import * as cluster from 'cluster';
import * as debug from 'debug'; import * as debug from 'debug';
import * as chalk from 'chalk'; import chalk from 'chalk';
// import portUsed = require('tcp-port-used'); // import portUsed = require('tcp-port-used');
import isRoot = require('is-root'); import isRoot = require('is-root');
import { master } from 'accesses'; import { master } from 'accesses';

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 Accesses from 'accesses';
import vhost = require('vhost'); import vhost = require('vhost');
import log from './log-request';
import config from './conf'; import config from './conf';
/** /**
@ -35,7 +36,12 @@ app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', {
stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null
})); }));
// Drop request that without 'Host' header app.use((req, res, next) => {
log(req);
next();
});
// Drop request when without 'Host' header
app.use((req, res, next) => { app.use((req, res, next) => {
if (!req.headers['host']) { if (!req.headers['host']) {
res.sendStatus(400); res.sendStatus(400);
@ -55,13 +61,17 @@ app.use(require('./web/server'));
/** /**
* Create server * Create server
*/ */
const server = config.https.enable ? const server = (() => {
https.createServer({ if (config.https) {
key: fs.readFileSync(config.https.key), const certs = {};
cert: fs.readFileSync(config.https.cert), Object.keys(config.https).forEach(k => {
ca: fs.readFileSync(config.https.ca) certs[k] = fs.readFileSync(config.https[k]);
}, app) : });
http.createServer(app); return https.createServer(certs, app);
} else {
return http.createServer(app);
}
})();
/** /**
* Steaming * Steaming

View File

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

View File

@ -1,8 +1,8 @@
import * as chalk from 'chalk'; import chalk, { Chalk } from 'chalk';
export type LogLevel = 'Error' | 'Warn' | 'Info'; export type LogLevel = 'Error' | 'Warn' | 'Info';
function toLevelColor(level: LogLevel): chalk.ChalkStyle { function toLevelColor(level: LogLevel): Chalk {
switch (level) { switch (level) {
case 'Error': return chalk.red; case 'Error': return chalk.red;
case 'Warn': return chalk.yellow; case 'Warn': return chalk.yellow;

View File

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

View File

@ -9,6 +9,7 @@ html
meta(name='application-name' content='Misskey') meta(name='application-name' content='Misskey')
meta(name='theme-color' content=themeColor) meta(name='theme-color' content=themeColor)
meta(name='referrer' content='origin') meta(name='referrer' content='origin')
link(rel='manifest' href='/manifest.json')
title Misskey title Misskey

View File

@ -27,7 +27,9 @@
// misskey.alice => misskey // misskey.alice => misskey
// misskey.strawberry.pasta => misskey // misskey.strawberry.pasta => misskey
// dev.misskey.arisu.tachibana => dev // dev.misskey.arisu.tachibana => dev
let app = url.host.split('.')[0]; let app = url.host == 'localhost'
? 'misskey'
: url.host.split('.')[0];
// Detect the user language // Detect the user language
// Note: The default language is English // Note: The default language is English

View File

@ -1,8 +1,8 @@
import * as riot from 'riot'; import * as riot from 'riot';
const route = require('page'); import * as route from 'page';
let page = null; let page = null;
export default me => { export default () => {
route('/', index); route('/', index);
route('/:channel', channel); route('/:channel', channel);
route('*', notFound); route('*', notFound);
@ -22,7 +22,7 @@ export default me => {
} }
// EXEC // EXEC
route(); (route as any)();
}; };
function mount(content) { function mount(content) {

View File

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

View File

@ -26,11 +26,11 @@
<hr> <hr>
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/> <mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
<div if={ !SIGNIN }> <div if={ !SIGNIN }>
<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p> <p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
</div> </div>
<hr> <hr>
<footer> <footer>
<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small> <small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
</footer> </footer>
</main> </main>
<style> <style>
@ -55,7 +55,7 @@
</style> </style>
<script> <script>
import Progress from '../../common/scripts/loading'; import Progress from '../../common/scripts/loading';
import ChannelStream from '../../common/scripts/channel-stream'; import ChannelStream from '../../common/scripts/streaming/channel-stream';
this.mixin('i'); this.mixin('i');
this.mixin('api'); this.mixin('api');
@ -66,7 +66,6 @@
this.channel = null; this.channel = null;
this.posts = null; this.posts = null;
this.connection = new ChannelStream(this.id); this.connection = new ChannelStream(this.id);
this.version = VERSION;
this.unreadCount = 0; this.unreadCount = 0;
this.on('mount', () => { this.on('mount', () => {
@ -166,7 +165,7 @@
<mk-channel-post> <mk-channel-post>
<header> <header>
<a class="index" onclick={ reply }>{ post.index }:</a> <a class="index" onclick={ reply }>{ post.index }:</a>
<a class="name" href={ CONFIG.url + '/' + post.user.username }><b>{ post.user.name }</b></a> <a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
<mk-time time={ post.created_at }/> <mk-time time={ post.created_at }/>
<mk-time time={ post.created_at } mode="detail"/> <mk-time time={ post.created_at } mode="detail"/>
<span>ID:<i>{ post.user.username }</i></span> <span>ID:<i>{ post.user.username }</i></span>
@ -284,8 +283,6 @@
</style> </style>
<script> <script>
import CONFIG from '../../common/scripts/config';
this.mixin('api'); this.mixin('api');
this.channel = this.opts.channel; this.channel = this.opts.channel;
@ -343,7 +340,7 @@
}; };
this.changeFile = () => { this.changeFile = () => {
this.refs.file.files.forEach(this.upload); Array.from(this.refs.file.files).forEach(this.upload);
}; };
this.selectFile = () => { this.selectFile = () => {
@ -357,7 +354,7 @@
}); });
}; };
window.open(CONFIG.url + '/selectdrive?multiple=true', window.open(_URL_ + '/selectdrive?multiple=true',
'drive_window', 'drive_window',
'height=500,width=800'); 'height=500,width=800');
}; };
@ -367,7 +364,7 @@
}; };
this.onpaste = e => { this.onpaste = e => {
e.clipboardData.items.forEach(item => { Array.from(e.clipboardData.items).forEach(item => {
if (item.kind == 'file') { if (item.kind == 'file') {
this.upload(item.getAsFile()); this.upload(item.getAsFile());
} }
@ -390,7 +387,7 @@
</mk-twitter-button> </mk-twitter-button>
<mk-line-button> <mk-line-button>
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div> <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
<script> <script>
this.on('mount', () => { this.on('mount', () => {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];

View File

@ -1,10 +1,10 @@
<mk-header> <mk-header>
<div> <div>
<a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a> <a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
</div> </div>
<div> <div>
<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a> <a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a> <a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
</div> </div>
<style> <style>
:scope :scope

View File

@ -15,7 +15,9 @@
this.mixin('api'); this.mixin('api');
this.on('mount', () => { this.on('mount', () => {
this.api('channels').then(channels => { this.api('channels', {
limit: 100
}).then(channels => {
this.update({ this.update({
channels: channels channels: channels
}); });

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 * API Request
*/ */
import CONFIG from './config'; declare const _API_URL_: string;
let spinner = null; let spinner = null;
let pending = 0; let pending = 0;
@ -14,7 +14,7 @@ let pending = 0;
* @param {any} [data={}] Data * @param {any} [data={}] Data
* @return {Promise<any>} Response * @return {Promise<any>} Response
*/ */
export default (i, endpoint, data = {}) => { export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
if (++pending === 1) { if (++pending === 1) {
spinner = document.createElement('div'); spinner = document.createElement('div');
spinner.setAttribute('id', 'wait'); spinner.setAttribute('id', 'wait');
@ -22,11 +22,11 @@ export default (i, endpoint, data = {}) => {
} }
// Append the credential // Append the credential
if (i != null) data.i = typeof i === 'object' ? i.token : i; if (i != null) (data as any).i = typeof i === 'object' ? i.token : i;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Send request // Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${CONFIG.apiUrl}/${endpoint}`, { fetch(endpoint.indexOf('://') > -1 ? endpoint : `${_API_URL_}/${endpoint}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
credentials: endpoint === 'signin' ? 'include' : 'omit' credentials: endpoint === 'signin' ? 'include' : 'omit'

View File

@ -1,6 +1,6 @@
export default (bytes, digits = 0) => { export default (bytes, digits = 0) => {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0Byte'; if (bytes == 0) return '0Byte';
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
}; };

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