diff --git a/.gitignore b/.gitignore index 2ae0f98c5..a51e70381 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ /.vscode /node_modules /built -/uploads /data npm-debug.log *.pem diff --git a/.travis/default.yml b/.travis/default.yml index 1875748d6..471a2a7c4 100644 --- a/.travis/default.yml +++ b/.travis/default.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/.travis/test.yml b/.travis/test.yml index f311310c7..6a115d6ab 100644 --- a/.travis/test.yml +++ b/.travis/test.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/CHANGELOG.md b/CHANGELOG.md index f8018e4e2..7dbd7797e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,219 @@ ChangeLog (Release Notes) ========================= 主に notable な changes を書いていきます +3201 (2017/11/23) +----------------- +* Twitterログインを実装 (#939) + +3196 (2017/11/23) +----------------- +* バグ修正 + +3194 (2017/11/23) +----------------- +* バグ修正 + +3191 (2017/11/23) +----------------- +* :v: + +3188 (2017/11/22) +----------------- +* バグ修正 + +3180 (2017/11/21) +----------------- +* バグ修正 + +3177 (2017/11/21) +----------------- +* ServiceWorker support + * Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ) + +3165 (2017/11/20) +----------------- +* デスクトップ版でも通知バッジを表示 (#918) +* デザインの調整 +* バグ修正 + +3155 (2017/11/20) +----------------- +* デスクトップ版でユーザーの投稿グラフを見れるように + +3142 (2017/11/18) +----------------- +* バグ修正 + +3140 (2017/11/18) +----------------- +* ウィジェットをスクロールに追従させるように + +3136 (2017/11/17) +----------------- +* バグ修正 +* 通信の最適化 + +3131 (2017/11/17) +----------------- +* バグ修正 +* 通信の最適化 + +3124 (2017/11/16) +----------------- +* バグ修正 + +3121 (2017/11/16) +----------------- +* ブロードキャストウィジェットの強化 +* デザインのグリッチの修正 +* 通信の最適化 + +3113 (2017/11/15) +----------------- +* アクティビティのレンダリングの問題の修正など + +3110 (2017/11/15) +----------------- +* デザインの調整など + +3107 (2017/11/14) +----------------- +* デザインの調整 + +3104 (2017/11/14) +----------------- +* デスクトップ版ユーザーページのデザインの改良 +* バグ修正 + +3099 (2017/11/14) +----------------- +* デスクトップ版ユーザーページの強化 +* バグ修正 + +3093 (2017/11/14) +----------------- +* やった + +3089 (2017/11/14) +----------------- +* なんか + +3069 (2017/11/14) +----------------- +* ドライブウィンドウもポップアウトできるように +* デザインの調整 + +3066 (2017/11/14) +----------------- +* メッセージウィジェット追加 +* アクセスログウィジェット追加 + +3057 (2017/11/13) +----------------- +* グリッチ修正 + +3055 (2017/11/13) +----------------- +* メッセージのウィンドウのポップアウト (#911) + +3050 (2017/11/13) +----------------- +* 通信の最適化 + * これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる +* デザインの調整 +* ユーザビリティの向上 + +3040 (2017/11/12) +----------------- +* バグ修正 + +3038 (2017/11/12) +----------------- +* 投稿フォームウィジェットの追加 +* タイムライン上部にもウィジェットを配置できるように + +3035 (2017/11/12) +----------------- +* ウィジェットの強化 + +3033 (2017/11/12) +----------------- +* デザインの調整 + +3031 (2017/11/12) +----------------- +* ウィジェットの強化 + +3028 (2017/11/12) +----------------- +* ウィジェットの表示をコンパクトにできるように + +3026 (2017/11/12) +----------------- +* バグ修正 + +3024 (2017/11/12) +----------------- +* いい感じにするなど + +3020 (2017/11/12) +----------------- +* 通信の最適化 + +3017 (2017/11/11) +----------------- +* 誤字修正など + +3012 (2017/11/11) +----------------- +* デザインの調整 + +3010 (2017/11/11) +----------------- +* デザインの調整 + +3008 (2017/11/11) +----------------- +* カレンダー(タイムマシン)ウィジェットの追加 + +3006 (2017/11/11) +----------------- +* デザインの調整 +* など + +2996 (2017/11/10) +----------------- +* デザインの調整 +* など + +2991 (2017/11/09) +----------------- +* デザインの調整 + +2988 (2017/11/09) +----------------- +* チャンネルウィジェットを追加 + +2984 (2017/11/09) +----------------- +* スライドショーウィジェットを追加 + +2974 (2017/11/08) +----------------- +* ホームのカスタマイズを実装するなど + +2971 (2017/11/08) +----------------- +* バグ修正 +* デザインの調整 +* i18n + +2944 (2017/11/07) +----------------- +* パフォーマンスの向上 + * GirdFSになるなどした +* 依存関係の更新 + 2807 (2017/11/02) ----------------- * いい感じに diff --git a/DONORS.md b/DONORS.md index da71c043a..9752068b3 100644 --- a/DONORS.md +++ b/DONORS.md @@ -1,5 +1,6 @@ DONORS ====== +The list of people who have sent donation for Misskey. (no particular order) @@ -7,12 +8,14 @@ DONORS * 俺様 * なぎうり * スルメ https://surume.tk/ +* 藍 +* 音船 https://otofune.me/ :heart: Thanks for donating, guys! --- -Although you donated, you are not listed here? please contact to us! +If your name is missing, please contact us! If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. diff --git a/README.md b/README.md index b777618f4..e3c93b749 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Key features * Automatically updated timeline * Private messages * Free 1GB storage for each all users -* Machine learning +* ServiceWorker support * Web API for third-party applications * No ads @@ -38,10 +38,18 @@ Please see [ChangeLog](./CHANGELOG.md). Sponsors & Backers ---------------------------------------------------------------- -Misskey have no 100+ GitHub stars currently. However, donation are always welcome! +Misskey has no 100+ GitHub stars currently. However, a donation is always welcome! If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. -**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md). +**Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md). + +Collaborators +---------------------------------------------------------------- +| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | +|------------------------|-----------------------------------|---------------------------------| +| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] | + +[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) Copyright ---------------------------------------------------------------- @@ -51,8 +59,8 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE). [mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square [travis-link]: https://travis-ci.org/syuilo/misskey [travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square -[dependencies-link]: https://gemnasium.com/syuilo/misskey -[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square +[dependencies-link]: https://david-dm.org/syuilo/misskey +[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square [himasaku]: https://himasaku.net [himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square [sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square @@ -60,3 +68,7 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE). [syuilo-link]: https://syuilo.com [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 +[ayamorisawa-link]: https://github.com/ayamorisawa +[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70 +[otofune-link]: https://github.com/otofune +[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 03a42b9b4..000000000 --- a/appveyor.yml +++ /dev/null @@ -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 diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 000000000..653fff1a7 --- /dev/null +++ b/docs/config.md @@ -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: + +``` diff --git a/docs/setup.en.md b/docs/setup.en.md index dbc0599b5..9c31e4f17 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -1,7 +1,7 @@ Misskey Setup and Installation Guide ================================================================ -We thank you for your interest in setup your Misskey server! +We thank you for your interest in setting up your Misskey server! This guide describes how to install and setup Misskey. [Japanese version also available - 日本語版もあります](./setup.ja.md) @@ -36,6 +36,15 @@ Note that Misskey uses following subdomains: Misskey requires reCAPTCHA tokens. Please visit https://www.google.com/recaptcha/intro/ and generate keys. +*(optional)* Generating VAPID keys +---------------------------------------------------------------- +If you want to enable ServiceWroker, you need to generate VAPID keys: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + *3.* Install dependencies ---------------------------------------------------------------- Please install and setup these softwares: @@ -51,24 +60,6 @@ Please install and setup these softwares: *4.* Install Misskey ---------------------------------------------------------------- -There is **two ways** to install Misskey: - -### WAY 1) Using built code (recommended) -We have official release of Misskey. -The built code is automatically pushed to https://github.com/syuilo/misskey/tree/release after the CI test succeeds. - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### Update -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### WAY 2) Using source code -If you want to build Misskey manually, you can do it via the -`build` command after download the source code of Misskey and install dependencies: 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 602fd9b6a..1e8bb553f 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -37,6 +37,15 @@ Misskeyは以下のサブドメインを使います: MisskeyはreCAPTCHAトークンを必要とします。 https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 +*(オプション)* VAPIDキーペアの生成 +---------------------------------------------------------------- +ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + *3.* 依存関係をインストールする ---------------------------------------------------------------- これらのソフトウェアをインストール・設定してください: @@ -52,26 +61,6 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生 *4.* Misskeyのインストール ---------------------------------------------------------------- -Misskeyをインストールするには**2つの方法**があります: - -### 方法 1) ビルドされたコードを利用する (推奨) -Misskeyには公式のリリースがあります。 -ビルドされたコードはCIテストに合格した後、自動で https://github.com/syuilo/misskey/tree/release にpushされています。 - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### アップデートするには: -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### 方法 2) ソースコードを利用する -> 注: この方法では正しくビルド・動作できることは保証されません。 - -Misskeyを手動でビルドしたい場合は、Misskeyのソースコードと依存関係をインストールした後、 -`build`コマンドを用いることができます: 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` diff --git a/gulpfile.ts b/gulpfile.ts index 4ee5fbce0..93002cbf3 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -13,7 +13,7 @@ import cssnano = require('gulp-cssnano'); import * as uglifyComposer from 'gulp-uglify/composer'; import pug = require('gulp-pug'); import * as rimraf from 'rimraf'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; import imagemin = require('gulp-imagemin'); import * as rename from 'gulp-rename'; import * as mocha from 'gulp-mocha'; @@ -81,9 +81,19 @@ gulp.task('lint', () => .pipe(tslint.report()) ); +gulp.task('format', () => +gulp.src('./src/**/*.ts') + .pipe(tslint({ + formatter: 'verbose', + fix: true + })) + .pipe(tslint.report()) +); + gulp.task('mocha', () => gulp.src([]) .pipe(mocha({ + exit: true //compilers: 'ts:ts-node/register' } as any)) ); @@ -123,7 +133,7 @@ gulp.task('build:client:script', () => .pipe(replace('VERSION', JSON.stringify(version))) .pipe(isProduction ? uglify({ toplevel: true - }) : gutil.noop()) + } as any) : gutil.noop()) .pipe(gulp.dest('./built/web/assets/')) as any ); diff --git a/locales/en.yml b/locales/en.yml index 52e8dfdb4..9a54eed67 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -13,6 +13,15 @@ common: months_ago: "{}month(s) ago" years_ago: "{}year(s) ago" + weekday-short: + sunday: "S" + monday: "M" + tuesday: "T" + wednesday: "W" + thursday: "T" + friday: "F" + satruday: "S" + reactions: like: "Like" love: "Love" @@ -41,6 +50,15 @@ common: my-token-regenerated: "Your token is just regenerated, so you will signout." tags: + mk-nav-links: + about: "About" + stats: "Stats" + status: "Status" + wiki: "Wiki" + donors: "Donors" + repository: "Repository" + develop: "Developers" + mk-messaging-form: attach-from-local: "Attach file from your pc" attach-from-drive: "Attach file from the drive" @@ -225,7 +243,6 @@ desktop: mk-drive-browser-file: avatar: "Avatar" banner: "Banner" - wallpaper: "Wallpaper" mk-drive-browser-folder-contextmenu: move-to-this-folder: "Move to this folder" @@ -242,14 +259,11 @@ desktop: mk-drive-browser-nav-folder: drive: "Drive" - mk-nav-home-widget: - about: "About" - stats: "Stats" - status: "Status" - wiki: "Wiki" - donors: "Donors" - repository: "Repository" - develop: "Developers" + mk-selectdrive-page: + title: "Choose a file(s)" + ok: "OK" + cancel: "Cancel" + upload: "Upload a file(s) from you PC" mk-ui-header-nav: home: "Home" @@ -267,6 +281,12 @@ desktop: settings: "Settings" signout: "Sign out" + mk-ui-header-post-button: + post: "Compose new Post" + + mk-ui-header-notifications: + title: "Notifications" + mk-password-setting: reset: "Change your password" enter-current-password: "Enter the current password" @@ -327,7 +347,7 @@ desktop: title: "Server info" toggle: "Toggle views" - mk-activity-home-widget: + mk-activity-widget: title: "Activity" toggle: "Toggle views" @@ -354,6 +374,34 @@ desktop: title: "Donation" text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!" + mk-channel-home-widget: + title: "Channel" + settings: "Widget settings" + get-started: "Please click the cog in the upper right to specify the channel to receive" + + mk-calendar-widget: + title: "{1} / {2}" + prev: "Previous month" + next: "Next month" + go: "Click to travel" + + mk-post-form-home-widget: + title: "Post" + post: "Post" + placeholder: "What's happening?" + + mk-access-log-home-widget: + title: "Access log" + + mk-messaging-home-widget: + title: "Messaging" + + mk-broadcast-home-widget: + fetching: "Fetching" + no-broadcasts: "No broadcasts" + have-a-nice-day: "Have a nice day!" + next: "Next" + mk-repost-form: quote: "Quote..." cancel: "Cancel" @@ -365,6 +413,24 @@ desktop: mk-repost-form-window: title: "Are you sure you want to repost this post?" + mk-user: + last-used-at: "Last used at" + + photos: + title: "Photos" + loading: "Loading" + no-photos: "No photos" + + frequently-replied-users: + title: "Frequently replied" + loading: "Loading" + no-users: "No users" + + followers-you-know: + title: "Followers you know" + loading: "Loading" + no-users: "No users" + mobile: tags: mk-selectdrive-page: @@ -374,7 +440,7 @@ mobile: download: "Download" rename: "Rename" move: "Move" - hash: "Hash" + hash: "Hash (md5)" mk-entrance-signin: signup: "Sign up" diff --git a/locales/ja.yml b/locales/ja.yml index dcd012bb8..dcf466339 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -13,6 +13,15 @@ common: months_ago: "{}ヶ月前" years_ago: "{}年前" + weekday-short: + sunday: "日" + monday: "月" + tuesday: "火" + wednesday: "水" + thursday: "木" + friday: "金" + satruday: "土" + reactions: like: "いいね" love: "ハート" @@ -41,6 +50,15 @@ common: my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" tags: + mk-nav-links: + about: "Misskeyについて" + stats: "統計" + status: "ステータス" + wiki: "Wiki" + donors: "ドナー" + repository: "リポジトリ" + develop: "開発者" + mk-messaging-form: attach-from-local: "PCからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する" @@ -225,7 +243,6 @@ desktop: mk-drive-browser-file: avatar: "アバター" banner: "バナー" - wallpaper: "壁紙" mk-drive-browser-folder-contextmenu: move-to-this-folder: "このフォルダへ移動" @@ -242,14 +259,11 @@ desktop: mk-drive-browser-nav-folder: drive: "ドライブ" - mk-nav-home-widget: - about: "Misskeyについて" - stats: "統計" - status: "ステータス" - wiki: "Wiki" - donors: "ドナー" - repository: "リポジトリ" - develop: "開発者" + mk-selectdrive-page: + title: "ファイルを選択してください" + ok: "決定" + cancel: "キャンセル" + upload: "PCからドライブにファイルをアップロード" mk-ui-header-nav: home: "ホーム" @@ -267,6 +281,12 @@ desktop: settings: "設定" signout: "サインアウト" + mk-ui-header-post-button: + post: "新規投稿" + + mk-ui-header-notifications: + title: "通知" + mk-password-setting: reset: "パスワードを変更する" enter-current-password: "現在のパスワードを入力してください" @@ -327,7 +347,7 @@ desktop: title: "サーバー情報" toggle: "表示を切り替え" - mk-activity-home-widget: + mk-activity-widget: title: "アクティビティ" toggle: "表示を切り替え" @@ -354,6 +374,34 @@ desktop: title: "寄付のお願い" text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。" + mk-channel-home-widget: + title: "チャンネル" + settings: "ウィジェットの設定" + get-started: "右上の歯車をクリックして受信するチャンネルを指定してください" + + mk-calendar-widget: + title: "{1}年 {2}月" + prev: "先月" + next: "来月" + go: "クリックして時間遡行" + + mk-post-form-home-widget: + title: "投稿" + post: "投稿" + placeholder: "いまどうしてる?" + + mk-access-log-home-widget: + title: "アクセスログ" + + mk-messaging-home-widget: + title: "メッセージ" + + mk-broadcast-home-widget: + fetching: "確認中" + no-broadcasts: "お知らせはありません" + have-a-nice-day: "良い一日を!" + next: "次" + mk-repost-form: quote: "引用する..." cancel: "キャンセル" @@ -365,6 +413,24 @@ desktop: mk-repost-form-window: title: "この投稿をRepostしますか?" + mk-user: + last-used-at: "最終アクセス" + + photos: + title: "フォト" + loading: "読み込み中" + no-photos: "写真はありません" + + frequently-replied-users: + title: "よく話すユーザー" + loading: "読み込み中" + no-users: "よく話すユーザーはいません" + + followers-you-know: + title: "知り合いのフォロワー" + loading: "読み込み中" + no-users: "知り合いのフォロワーはいません" + mobile: tags: mk-selectdrive-page: @@ -374,7 +440,7 @@ mobile: download: "ダウンロード" rename: "名前を変更" move: "移動" - hash: "ハッシュ" + hash: "ハッシュ (md5)" mk-entrance-signin: signup: "新規登録" diff --git a/package.json b/package.json index 051eb1cb8..7d302279e 100644 --- a/package.json +++ b/package.json @@ -1,158 +1,166 @@ { - "name": "misskey", - "author": "syuilo ", - "version": "0.0.2807", - "license": "MIT", - "description": "A miniblog-based SNS", - "bugs": "https://github.com/syuilo/misskey/issues", - "repository": "https://github.com/syuilo/misskey.git", - "main": "./built/index.js", - "private": true, - "scripts": { - "config": "node ./tools/init.js", - "start": "node ./built", - "debug": "DEBUG=misskey:* node ./built", - "swagger": "node ./swagger.js", - "build": "gulp build", - "rebuild": "gulp rebuild", - "clean": "gulp clean", - "cleanall": "gulp cleanall", - "lint": "gulp lint", - "test": "gulp test" - }, - "devDependencies": { - "@types/bcryptjs": "2.4.0", - "@types/body-parser": "1.16.5", - "@types/chai": "4.0.4", - "@types/chai-http": "3.0.3", - "@types/chalk": "0.4.31", - "@types/compression": "0.0.34", - "@types/cors": "2.8.1", - "@types/debug": "0.0.30", - "@types/deep-equal": "1.0.1", - "@types/elasticsearch": "5.0.14", - "@types/event-stream": "3.3.32", - "@types/express": "4.0.37", - "@types/gm": "1.17.32", - "@types/gulp": "4.0.3", - "@types/gulp-htmlmin": "1.3.30", - "@types/gulp-mocha": "0.0.30", - "@types/gulp-rename": "0.0.32", - "@types/gulp-replace": "0.0.30", - "@types/gulp-tslint": "3.6.31", - "@types/gulp-typescript": "2.13.0", - "@types/gulp-uglify": "0.0.30", - "@types/gulp-util": "3.0.31", - "@types/inquirer": "0.0.34", - "@types/is-root": "1.0.0", - "@types/is-url": "1.2.28", - "@types/js-yaml": "3.9.0", - "@types/mocha": "2.2.43", - "@types/mongodb": "2.2.13", - "@types/monk": "1.0.6", - "@types/morgan": "1.7.33", - "@types/ms": "0.7.30", - "@types/multer": "1.3.2", - "@types/node": "8.0.33", - "@types/ratelimiter": "2.1.28", - "@types/redis": "2.6.0", - "@types/request": "2.0.4", - "@types/rimraf": "2.0.0", - "@types/riot": "3.6.0", - "@types/serve-favicon": "2.2.28", - "@types/uuid": "3.4.2", - "@types/webpack": "3.0.13", - "@types/webpack-stream": "3.2.7", - "@types/websocket": "0.0.34", - "awesome-typescript-loader": "3.2.3", - "chai": "4.1.2", - "chai-http": "3.0.0", - "css-loader": "0.28.7", - "event-stream": "3.3.4", - "gulp": "3.9.1", - "gulp-cssnano": "2.1.2", - "gulp-htmlmin": "3.0.0", - "gulp-imagemin": "3.4.0", - "gulp-mocha": "4.3.1", - "gulp-pug": "3.3.0", - "gulp-rename": "1.2.2", - "gulp-replace": "0.6.1", - "gulp-tslint": "8.1.2", - "gulp-typescript": "3.2.2", - "gulp-uglify": "3.0.0", - "gulp-util": "3.0.8", - "mocha": "3.5.3", - "riot-tag-loader": "1.0.0", - "string-replace-webpack-plugin": "0.1.3", - "style-loader": "0.19.0", - "stylus": "0.54.5", - "stylus-loader": "3.0.1", - "swagger-jsdoc": "1.9.7", - "tslint": "5.7.0", - "uglify-es": "3.0.27", - "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", - "uglifyjs-webpack-plugin": "1.0.0-beta.2", - "webpack": "3.8.1" - }, - "dependencies": { - "accesses": "2.5.0", - "animejs": "2.2.0", - "autwh": "0.0.1", - "bcryptjs": "2.4.3", - "body-parser": "1.18.2", - "cafy": "3.0.0", - "chalk": "2.1.0", - "compression": "1.7.1", - "cors": "2.8.4", - "cropperjs": "1.1.3", - "crypto": "1.0.1", - "debug": "3.1.0", - "deep-equal": "1.0.1", - "deepcopy": "0.6.3", - "diskusage": "0.2.2", - "download": "6.2.5", - "elasticsearch": "13.3.1", - "escape-regexp": "0.0.1", - "express": "4.15.4", - "file-type": "6.2.0", - "fuckadblock": "3.2.1", - "gm": "1.23.0", - "inquirer": "3.3.0", - "is-root": "1.0.0", - "is-url": "1.2.2", - "js-yaml": "3.10.0", - "mecab-async": "^0.1.0", - "moji": "^0.5.1", - "mongodb": "2.2.33", - "monk": "6.0.5", - "morgan": "1.9.0", - "ms": "2.0.0", - "multer": "1.3.0", - "nprogress": "0.2.0", - "os-utils": "0.0.14", - "page": "1.7.1", - "pictograph": "2.0.4", - "prominence": "0.2.0", - "pug": "2.0.0-rc.4", - "ratelimiter": "3.0.3", - "recaptcha-promise": "0.1.3", - "reconnecting-websocket": "3.2.2", - "redis": "2.8.0", - "request": "2.83.0", - "rimraf": "2.6.2", - "riot": "3.7.3", - "rndstr": "1.0.0", - "s-age": "1.1.0", - "serve-favicon": "2.4.5", - "summaly": "2.0.3", - "syuilo-password-strength": "0.0.1", - "tcp-port-used": "0.1.2", - "textarea-caret": "3.0.2", - "ts-node": "3.3.0", - "typescript": "2.5.3", - "uuid": "3.1.0", - "vhost": "3.0.2", - "websocket": "1.0.25", - "xev": "2.0.0" - } + "name": "misskey", + "author": "syuilo ", + "version": "0.0.3201", + "license": "MIT", + "description": "A miniblog-based SNS", + "bugs": "https://github.com/syuilo/misskey/issues", + "repository": "https://github.com/syuilo/misskey.git", + "main": "./built/index.js", + "private": true, + "scripts": { + "config": "node ./tools/init.js", + "start": "node ./built", + "debug": "DEBUG=misskey:* node ./built", + "swagger": "node ./swagger.js", + "build": "gulp build", + "rebuild": "gulp rebuild", + "clean": "gulp clean", + "cleanall": "gulp cleanall", + "lint": "gulp lint", + "test": "gulp test", + "format": "gulp format" + }, + "dependencies": { + "@prezzemolo/rap": "0.1.2", + "@prezzemolo/zip": "0.0.3", + "@types/bcryptjs": "2.4.1", + "@types/body-parser": "1.16.8", + "@types/chai": "4.0.5", + "@types/chai-http": "3.0.3", + "@types/compression": "0.0.35", + "@types/cookie": "0.3.1", + "@types/cors": "2.8.3", + "@types/debug": "0.0.30", + "@types/deep-equal": "1.0.1", + "@types/elasticsearch": "5.0.17", + "@types/event-stream": "3.3.33", + "@types/eventemitter3": "2.0.2", + "@types/express": "4.0.39", + "@types/gm": "1.17.33", + "@types/gulp": "4.0.3", + "@types/gulp-htmlmin": "1.3.31", + "@types/gulp-mocha": "0.0.31", + "@types/gulp-rename": "0.0.33", + "@types/gulp-replace": "0.0.31", + "@types/gulp-uglify": "3.0.3", + "@types/gulp-util": "3.0.34", + "@types/inquirer": "0.0.35", + "@types/is-root": "1.0.0", + "@types/is-url": "1.2.28", + "@types/js-yaml": "3.10.0", + "@types/mocha": "2.2.44", + "@types/mongodb": "2.2.15", + "@types/monk": "1.0.6", + "@types/morgan": "1.7.35", + "@types/ms": "0.7.30", + "@types/multer": "1.3.6", + "@types/node": "8.0.53", + "@types/page": "1.5.32", + "@types/proxy-addr": "2.0.0", + "@types/ratelimiter": "2.1.28", + "@types/redis": "2.8.1", + "@types/request": "2.0.7", + "@types/rimraf": "2.0.2", + "@types/riot": "3.6.1", + "@types/seedrandom": "2.4.27", + "@types/serve-favicon": "2.2.30", + "@types/tmp": "0.0.33", + "@types/uuid": "3.4.3", + "@types/webpack": "3.8.1", + "@types/webpack-stream": "3.2.8", + "@types/websocket": "0.0.34", + "accesses": "2.5.0", + "animejs": "2.2.0", + "autwh": "0.0.1", + "awesome-typescript-loader": "3.4.0", + "bcryptjs": "2.4.3", + "body-parser": "1.18.2", + "cafy": "3.2.0", + "chai": "4.1.2", + "chai-http": "3.0.0", + "chalk": "2.3.0", + "compression": "1.7.1", + "cookie": "0.3.1", + "cors": "2.8.4", + "cropperjs": "1.1.3", + "css-loader": "0.28.7", + "debug": "3.1.0", + "deep-equal": "1.0.1", + "deepcopy": "0.6.3", + "diskusage": "0.2.4", + "elasticsearch": "14.0.0", + "escape-regexp": "0.0.1", + "event-stream": "3.3.4", + "eventemitter3": "2.0.3", + "express": "4.16.2", + "file-type": "7.3.0", + "fuckadblock": "3.2.1", + "gm": "1.23.0", + "gulp": "3.9.1", + "gulp-cssnano": "2.1.2", + "gulp-htmlmin": "3.0.0", + "gulp-imagemin": "4.0.0", + "gulp-mocha": "4.3.1", + "gulp-pug": "3.3.0", + "gulp-rename": "1.2.2", + "gulp-replace": "0.6.1", + "gulp-tslint": "8.1.2", + "gulp-typescript": "3.2.3", + "gulp-uglify": "3.0.0", + "gulp-util": "3.0.8", + "inquirer": "4.0.0", + "is-root": "1.0.0", + "is-url": "1.2.2", + "js-yaml": "3.10.0", + "mecab-async": "0.1.0", + "mocha": "4.0.1", + "moji": "0.5.1", + "mongodb": "2.2.33", + "monk": "6.0.5", + "morgan": "1.9.0", + "ms": "2.0.0", + "multer": "1.3.0", + "nprogress": "0.2.0", + "os-utils": "0.0.14", + "page": "1.7.1", + "pictograph": "2.1.2", + "prominence": "0.2.0", + "proxy-addr": "2.0.2", + "pug": "2.0.0-rc.4", + "ratelimiter": "3.0.3", + "recaptcha-promise": "0.1.3", + "reconnecting-websocket": "3.2.2", + "redis": "2.8.0", + "request": "2.83.0", + "rimraf": "2.6.2", + "riot": "3.7.4", + "riot-tag-loader": "1.0.0", + "rndstr": "1.0.0", + "s-age": "1.1.0", + "seedrandom": "^2.4.3", + "serve-favicon": "2.4.5", + "sortablejs": "1.7.0", + "string-replace-webpack-plugin": "0.1.3", + "style-loader": "0.19.0", + "stylus": "0.54.5", + "stylus-loader": "3.0.1", + "summaly": "2.0.3", + "swagger-jsdoc": "1.9.7", + "syuilo-password-strength": "0.0.1", + "tcp-port-used": "0.1.2", + "textarea-caret": "3.0.2", + "tmp": "0.0.33", + "ts-node": "3.3.0", + "tslint": "5.8.0", + "typescript": "2.6.1", + "uglify-es": "3.1.10", + "uglifyjs-webpack-plugin": "1.1.1", + "uuid": "3.1.0", + "vhost": "3.0.2", + "web-push": "3.2.4", + "webpack": "3.8.1", + "websocket": "1.0.25", + "xev": "2.0.0" + } } diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 53fb18119..ddae6405f 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -5,6 +5,7 @@ import User, { IUser, init as initUser } from '../models/user'; import getPostSummary from '../../common/get-post-summary'; import getUserSummary from '../../common/get-user-summary'; +import getNotificationSummary from '../../common/get-notification-summary'; import Othello, { ai as othelloAi } from '../../common/othello'; @@ -62,7 +63,7 @@ export default class BotCore extends EventEmitter { return bot; } - public async q(query: string): Promise { + public async q(query: string): Promise { if (this.context != null) { return await this.context.q(query); } @@ -84,7 +85,10 @@ export default class BotCore extends EventEmitter { 'logout, signout: サインアウトします\n' + 'post: 投稿します\n' + 'tl: タイムラインを見ます\n' + - '@<ユーザー名>: ユーザーを表示します'; + 'no: 通知を見ます\n' + + '@<ユーザー名>: ユーザーを表示します\n' + + '\n' + + 'タイムラインや通知を見た後、「次」というとさらに遡ることができます。'; case 'me': return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; @@ -113,7 +117,16 @@ export default class BotCore extends EventEmitter { case 'tl': case 'タイムライン': - return await this.tlCommand(); + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new TlContext(this)); + return await this.context.greet(); + + case 'no': + case 'notifications': + case '通知': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new NotificationsContext(this)); + return await this.context.greet(); case 'guessing-game': case '数当てゲーム': @@ -155,21 +168,7 @@ export default class BotCore extends EventEmitter { this.emit('updated'); } - public async tlCommand(): Promise { - 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 { + public async showUserCommand(q: string): Promise { try { const user = await require('../endpoints/users/show')({ username: q.substr(1) @@ -200,6 +199,8 @@ abstract class Context extends EventEmitter { if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content); if (data.type == 'othello') return OthelloContext.import(bot, data.content); if (data.type == 'post') return PostContext.import(bot, data.content); + if (data.type == 'tl') return TlContext.import(bot, data.content); + if (data.type == 'notifications') return NotificationsContext.import(bot, data.content); if (data.type == 'signin') return SigninContext.import(bot, data.content); return null; } @@ -232,7 +233,7 @@ class SigninContext extends Context { } } else { // Compare password - const same = bcrypt.compareSync(query, this.temporaryUser.password); + const same = await bcrypt.compare(query, this.temporaryUser.password); if (same) { this.bot.signin(this.temporaryUser); @@ -285,6 +286,110 @@ class PostContext extends Context { } } +class TlContext extends Context { + private next: string = null; + + public async greet(): Promise { + return await this.getTl(); + } + + public async q(query: string): Promise { + 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 { + return await this.getNotifications(); + } + + public async q(query: string): Promise { + if (query == '次') { + return await this.getNotifications(); + } else { + this.bot.clearContext(); + return await this.bot.q(query); + } + } + + private async getNotifications() { + const notifications = await require('../endpoints/i/notifications')({ + limit: 5, + max_id: this.next ? this.next : undefined + }, this.bot.user); + + if (notifications.length > 0) { + this.next = notifications[notifications.length - 1].id; + this.emit('updated'); + + const text = notifications + .map(notification => getNotificationSummary(notification)) + .join('\n-----\n'); + + return text; + } else { + return '通知はありません'; + } + } + + public export() { + return { + type: 'notifications', + content: { + next: this.next, + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new NotificationsContext(bot); + context.next = data.next; + return context; + } +} + class GuessingGameContext extends Context { private secret: number; private history: number[] = []; diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 0caa71ed2..43c25f803 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -135,6 +135,8 @@ class LineBot extends BotCore { actions: actions } }]); + + return null; } public async showUserTimelinePostback(userId: string) { diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts index 714eeb520..2a649788a 100644 --- a/src/api/common/add-file-to-drive.ts +++ b/src/api/common/add-file-to-drive.ts @@ -1,172 +1,264 @@ +import { Buffer } from 'buffer'; +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import * as stream from 'stream'; + import * as mongodb from 'mongodb'; import * as crypto from 'crypto'; import * as gm from 'gm'; import * as debug from 'debug'; import fileType = require('file-type'); import prominence = require('prominence'); -import DriveFile from '../models/drive-file'; + +import DriveFile, { getGridFSBucket } from '../models/drive-file'; import DriveFolder from '../models/drive-folder'; import serialize from '../serializers/drive-file'; -import event from '../event'; +import event, { publishDriveStream } from '../event'; import config from '../../conf'; const log = debug('misskey:register-drive-file'); +const tmpFile = (): Promise => 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 => + 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 => 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 => new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }))() + ]); + + log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + + // detect name + const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled'); + + if (!force) { + // Check if there is a file with the same hash + const much = await DriveFile.findOne({ + md5: hash, + 'metadata.user_id': user._id + }); + + if (much !== null) { + log('file with same hash is found'); + return much; + } else { + log('file with same hash is not found'); + } + } + + const [properties, folder] = await Promise.all([ + // properties + (async () => { + // 画像かどうか + if (!/^image\/.*$/.test(mime)) { + return null; + } + + const imageType = mime.split('/')[1]; + + // 画像でもPNGかJPEGでないならスキップ + if (imageType != 'png' && imageType != 'jpeg') { + return null; + } + + // If the file is an image, calculate width and height to save in property + const g = gm(fs.createReadStream(path), name); + const size = await prominence(g).size(); + const properties = { + width: size.width, + height: size.height + }; + + log('image width and height is calculated'); + + return properties; + })(), + // folder + (async () => { + if (!folderId) { + return null; + } + const driveFolder = await DriveFolder.findOne({ + _id: folderId, + user_id: user._id + }); + if (!driveFolder) { + throw 'folder-not-found'; + } + return driveFolder; + })(), + // usage checker + (async () => { + // Calculate drive usage + const usage = await DriveFile + .aggregate([{ + $match: { 'metadata.user_id': user._id } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); + + log(`drive usage is ${usage}`); + + // If usage limit exceeded + if (usage + size > user.drive_capacity) { + throw 'no-free-space'; + } + })() + ]); + + const readable = fs.createReadStream(path); + + return addToGridFS(detectedName, readable, mime, { + user_id: user._id, + folder_id: folder !== null ? folder._id : null, + comment: comment, + properties: properties + }); +}; + /** * Add file to drive * * @param user User who wish to add file - * @param fileName File name - * @param data Contents + * @param file File path or readableStream * @param comment Comment * @param type File type * @param folderId Folder ID * @param force If set to true, forcibly upload the file even if there is a file with the same hash. * @return Object that represents added file */ -export default ( - user: any, - data: Buffer, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, - force: boolean = false -) => new Promise(async (resolve, reject) => { - log(`registering ${name} (user: ${user.username})`); - - // File size - const size = data.byteLength; - - log(`size is ${size}`); - - // File type - let mime = 'application/octet-stream'; - const type = fileType(data); - if (type !== null) { - mime = type.mime; - - if (name === null) { - name = `untitled.${type.ext}`; +export default (user: any, file: string | stream.Readable, ...args) => new Promise((resolve, reject) => { + // Get file path + new Promise((res: (v: [string, boolean]) => void, rej) => { + if (typeof file === 'string') { + res([file, false]); + return; } - } else { - if (name === null) { - name = 'untitled'; + if (typeof file === 'object' && typeof file.read === 'function') { + tmpFile() + .then(path => { + const readable: stream.Readable = file; + const writable = fs.createWriteStream(path); + readable + .on('error', rej) + .on('end', () => { + res([path, true]); + }) + .pipe(writable) + .on('error', rej); + }) + .catch(rej); } - } + rej(new Error('un-compatible file.')); + }) + .then(([path, remove]): Promise => new Promise((res, rej) => { + addFile(user, path, ...args) + .then(file => { + res(file); + if (remove) { + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + } + }) + .catch(rej); + })) + .then(file => { + log(`drive file has been created ${file._id}`); + resolve(file); - log(`type is ${mime}`); + serialize(file).then(serializedFile => { + // Publish drive_file_created event + event(user._id, 'drive_file_created', serializedFile); + publishDriveStream(user._id, 'file_created', serializedFile); - // Generate hash - const hash = crypto - .createHash('sha256') - .update(data) - .digest('hex') as string; - - log(`hash is ${hash}`); - - if (!force) { - // Check if there is a file with the same hash - const much = await DriveFile.findOne({ - user_id: user._id, - hash: hash - }); - - if (much !== null) { - log('file with same hash is found'); - return resolve(much); - } else { - log('file with same hash is not found'); - } - } - - // Calculate drive usage - const usage = ((await DriveFile - .aggregate([ - { $match: { user_id: user._id } }, - { $project: { - datasize: true - }}, - { $group: { - _id: null, - usage: { $sum: '$datasize' } - }} - ]))[0] || { - usage: 0 - }).usage; - - log(`drive usage is ${usage}`); - - // If usage limit exceeded - if (usage + size > user.drive_capacity) { - return reject('no-free-space'); - } - - // If the folder is specified - let folder: any = null; - if (folderId !== null) { - folder = await DriveFolder - .findOne({ - _id: folderId, - user_id: user._id - }); - - if (folder === null) { - return reject('folder-not-found'); - } - } - - let properties: any = null; - - // If the file is an image - if (/^image\/.*$/.test(mime)) { - // Calculate width and height to save in property - const g = gm(data, name); - const size = await prominence(g).size(); - properties = { - width: size.width, - height: size.height - }; - - log('image width and height is calculated'); - } - - // Create DriveFile document - const file = await DriveFile.insert({ - created_at: new Date(), - user_id: user._id, - folder_id: folder !== null ? folder._id : null, - data: data, - datasize: size, - type: mime, - name: name, - comment: comment, - hash: hash, - properties: properties - }); - - delete file.data; - - log(`drive file has been created ${file._id}`); - - resolve(file); - - // Serialize - const fileObj = await serialize(file); - - // Publish drive_file_created event - event(user._id, 'drive_file_created', fileObj); - - // Register to search database - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'drive_file', - id: file._id.toString(), - body: { - name: file.name, - user_id: user._id.toString() + // Register to search database + if (config.elasticsearch.enable) { + const es = require('../../db/elasticsearch'); + es.index({ + index: 'misskey', + type: 'drive_file', + id: file._id.toString(), + body: { + name: file.name, + user_id: user._id.toString() + } + }); } }); - } + }) + .catch(reject); }); diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts index e7ec37d4e..4b3e6a5d5 100644 --- a/src/api/common/notify.ts +++ b/src/api/common/notify.ts @@ -27,4 +27,12 @@ export default ( // Publish notification event event(notifiee, 'notification', await serialize(notification)); + + // 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する + setTimeout(async () => { + const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true }); + if (!fresh.is_read) { + event(notifiee, 'unread_notification', await serialize(notification)); + } + }, 3000); }); diff --git a/src/api/common/push-sw.ts b/src/api/common/push-sw.ts new file mode 100644 index 000000000..2993c760e --- /dev/null +++ b/src/api/common/push-sw.ts @@ -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 + }); + } + }); + }); +} diff --git a/src/api/common/read-messaging-message.ts b/src/api/common/read-messaging-message.ts index 3257ec8b0..8e5e5b2b6 100644 --- a/src/api/common/read-messaging-message.ts +++ b/src/api/common/read-messaging-message.ts @@ -3,6 +3,7 @@ import Message from '../models/messaging-message'; import { IMessagingMessage as IMessage } from '../models/messaging-message'; import publishUserStream from '../event'; import { publishMessagingStream } from '../event'; +import { publishMessagingIndexStream } from '../event'; /** * Mark as read message(s) @@ -49,6 +50,7 @@ export default ( // Publish event publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); + publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString())); // Calc count of my unread messages const count = await Message diff --git a/src/api/common/signin.ts b/src/api/common/signin.ts new file mode 100644 index 000000000..693e62f39 --- /dev/null +++ b/src/api/common/signin.ts @@ -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); + } +} diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index afefce39e..06fb9a64a 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -146,6 +146,11 @@ const endpoints: Endpoint[] = [ name: 'aggregation/posts/reactions' }, + { + name: 'sw/register', + withCredential: true + }, + { name: 'i', withCredential: true @@ -159,6 +164,11 @@ const endpoints: Endpoint[] = [ }, kind: 'account-write' }, + { + name: 'i/update_home', + withCredential: true, + kind: 'account-write' + }, { name: 'i/change_password', withCredential: true diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts index 498c4f144..ca684de02 100644 --- a/src/api/endpoints/app/create.ts +++ b/src/api/endpoints/app/create.ts @@ -85,7 +85,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (permissionErr) return rej('invalid permission param'); // Get 'callback_url' parameter - // TODO: Check $ is valid url + // TODO: Check it is valid url const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$; if (callbackUrlErr) return rej('invalid callback_url param'); diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts index fa91fb93e..5c071a124 100644 --- a/src/api/endpoints/channels/posts.ts +++ b/src/api/endpoints/channels/posts.ts @@ -3,7 +3,7 @@ */ import $ from 'cafy'; import { default as Channel, IChannel } from '../../models/channel'; -import { default as Post, IPost } from '../../models/post'; +import Post from '../../models/post'; import serialize from '../../serializers/post'; /** diff --git a/src/api/endpoints/drive.ts b/src/api/endpoints/drive.ts index 41ad6301d..d92473633 100644 --- a/src/api/endpoints/drive.ts +++ b/src/api/endpoints/drive.ts @@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Calculate drive usage const usage = ((await DriveFile .aggregate([ - { $match: { user_id: user._id } }, + { $match: { 'metadata.user_id': user._id } }, { $project: { - datasize: true + length: true } }, { $group: { _id: null, - usage: { $sum: '$datasize' } + usage: { $sum: '$length' } } } ]))[0] || { diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts index a68ae3481..b2e094775 100644 --- a/src/api/endpoints/drive/files.ts +++ b/src/api/endpoints/drive/files.ts @@ -13,35 +13,39 @@ import serialize from '../../serializers/drive-file'; * @param {any} app * @return {Promise} */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { +module.exports = async (params, user, app) => { // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); + if (limitErr) throw 'invalid limit param'; // Get 'since_id' parameter const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + if (sinceIdErr) throw 'invalid since_id param'; // Get 'max_id' parameter const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + if (maxIdErr) throw 'invalid max_id param'; // Check if both of since_id and max_id is specified if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + throw 'cannot set since_id and max_id'; } // Get 'folder_id' parameter const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + if (folderIdErr) throw 'invalid folder_id param'; + + // Get 'type' parameter + const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$; + if (typeErr) throw 'invalid type param'; // Construct query const sort = { _id: -1 }; const query = { - user_id: user._id, - folder_id: folderId + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId } as any; if (sinceId) { sort._id = 1; @@ -53,18 +57,18 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { $lt: maxId }; } + if (type) { + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + } // Issue query const files = await DriveFile .find(query, { - fields: { - data: false - }, limit: limit, sort: sort }); // Serialize - res(await Promise.all(files.map(async file => - await serialize(file)))); -}); + const _files = await Promise.all(files.map(file => serialize(file))); + return _files; +}; diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts index 43dca7762..7546eca30 100644 --- a/src/api/endpoints/drive/files/create.ts +++ b/src/api/endpoints/drive/files/create.ts @@ -1,7 +1,6 @@ /** * Module dependencies */ -import * as fs from 'fs'; import $ from 'cafy'; import { validateFileName } from '../../../models/drive-file'; import serialize from '../../../serializers/drive-file'; @@ -15,14 +14,11 @@ import create from '../../../common/add-file-to-drive'; * @param {any} user * @return {Promise} */ -module.exports = (file, params, user) => new Promise(async (res, rej) => { +module.exports = async (file, params, user): Promise => { if (file == null) { - return rej('file is required'); + throw 'file is required'; } - const buffer = fs.readFileSync(file.path); - fs.unlink(file.path, (err) => { if (err) console.log(err); }); - // Get 'name' parameter let name = file.originalname; if (name !== undefined && name !== null) { @@ -32,7 +28,7 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => { } else if (name === 'blob') { name = null; } else if (!validateFileName(name)) { - return rej('invalid name'); + throw 'invalid name'; } } else { name = null; @@ -40,14 +36,11 @@ module.exports = (file, params, user) => new Promise(async (res, rej) => { // Get 'folder_id' parameter const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + if (folderIdErr) throw 'invalid folder_id param'; // Create file - const driveFile = await create(user, buffer, name, null, folderId); + const driveFile = await create(user, file.path, name, null, folderId); // Serialize - const fileObj = await serialize(driveFile); - - // Response - res(fileObj); -}); + return serialize(driveFile); +}; diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts index cd0b33f2c..a1cdf1643 100644 --- a/src/api/endpoints/drive/files/find.ts +++ b/src/api/endpoints/drive/files/find.ts @@ -24,13 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Issue query const files = await DriveFile .find({ - name: name, - user_id: user._id, - folder_id: folderId - }, { - fields: { - data: false - } + filename: name, + 'metadata.user_id': user._id, + 'metadata.folder_id': folderId }); // Serialize diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts index 8dbc297e4..3c7cf774f 100644 --- a/src/api/endpoints/drive/files/show.ts +++ b/src/api/endpoints/drive/files/show.ts @@ -12,28 +12,26 @@ import serialize from '../../../serializers/drive-file'; * @param {any} user * @return {Promise} */ -module.exports = (params, user) => new Promise(async (res, rej) => { +module.exports = async (params, user) => { // Get 'file_id' parameter const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); + if (fileIdErr) throw 'invalid file_id param'; // Fetch file const file = await DriveFile .findOne({ _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } + 'metadata.user_id': user._id }); if (file === null) { - return rej('file-not-found'); + throw 'file-not-found'; } // Serialize - res(await serialize(file, { + const _file = await serialize(file, { detail: true - })); -}); + }); + + return _file; +}; diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts index 1cfbdd8f0..f39a420d6 100644 --- a/src/api/endpoints/drive/files/update.ts +++ b/src/api/endpoints/drive/files/update.ts @@ -6,7 +6,7 @@ import DriveFolder from '../../../models/drive-folder'; import DriveFile from '../../../models/drive-file'; import { validateFileName } from '../../../models/drive-file'; import serialize from '../../../serializers/drive-file'; -import event from '../../../event'; +import { publishDriveStream } from '../../../event'; /** * Update a file @@ -24,11 +24,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const file = await DriveFile .findOne({ _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } + 'metadata.user_id': user._id }); if (file === null) { @@ -38,7 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; if (nameErr) return rej('invalid name param'); - if (name) file.name = name; + if (name) file.filename = name; // Get 'folder_id' parameter const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; @@ -46,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (folderId !== undefined) { if (folderId === null) { - file.folder_id = null; + file.metadata.folder_id = null; } else { // Fetch folder const folder = await DriveFolder @@ -59,14 +55,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('folder-not-found'); } - file.folder_id = folder._id; + file.metadata.folder_id = folder._id; } } - DriveFile.update(file._id, { + await DriveFile.update(file._id, { $set: { - name: file.name, - folder_id: file.folder_id + filename: file.filename, + 'metadata.folder_id': file.metadata.folder_id } }); @@ -76,6 +72,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Response res(fileObj); - // Publish drive_file_updated event - event(user._id, 'drive_file_updated', fileObj); + // Publish file_updated event + publishDriveStream(user._id, 'file_updated', fileObj); }); diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts index 46cfffb69..519e0bdf6 100644 --- a/src/api/endpoints/drive/files/upload_from_url.ts +++ b/src/api/endpoints/drive/files/upload_from_url.ts @@ -2,11 +2,16 @@ * Module dependencies */ import * as URL from 'url'; -const download = require('download'); import $ from 'cafy'; import { validateFileName } from '../../../models/drive-file'; import serialize from '../../../serializers/drive-file'; import create from '../../../common/add-file-to-drive'; +import * as debug from 'debug'; +import * as tmp from 'tmp'; +import * as fs from 'fs'; +import * as request from 'request'; + +const log = debug('misskey:endpoint:upload_from_url'); /** * Create a file from a URL @@ -15,11 +20,11 @@ import create from '../../../common/add-file-to-drive'; * @param {any} user * @return {Promise} */ -module.exports = (params, user) => new Promise(async (res, rej) => { +module.exports = async (params, user): Promise => { // Get 'url' parameter // TODO: Validate this url const [url, urlErr] = $(params.url).string().$; - if (urlErr) return rej('invalid url param'); + if (urlErr) throw 'invalid url param'; let name = URL.parse(url).pathname.split('/').pop(); if (!validateFileName(name)) { @@ -28,17 +33,35 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'folder_id' parameter const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); + if (folderIdErr) throw 'invalid folder_id param'; - // Download file - const data = await download(url); + // Create temp file + const path = await new Promise((res: (string) => void, rej) => { + tmp.file((e, path) => { + if (e) return rej(e); + res(path); + }); + }); - // Create file - const driveFile = await create(user, data, name, null, folderId); + // write content at URL to temp file + await new Promise((res, rej) => { + const writable = fs.createWriteStream(path); + request(url) + .on('error', rej) + .on('end', () => { + writable.close(); + res(path); + }) + .pipe(writable) + .on('error', rej); + }); - // Serialize - const fileObj = await serialize(driveFile); + const driveFile = await create(user, path, name, null, folderId); - // Response - res(fileObj); -}); + // clean-up + fs.unlink(path, (e) => { + if (e) log(e.stack); + }); + + return serialize(driveFile); +}; diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts index 8c875db16..be847b215 100644 --- a/src/api/endpoints/drive/folders/create.ts +++ b/src/api/endpoints/drive/folders/create.ts @@ -5,7 +5,7 @@ import $ from 'cafy'; import DriveFolder from '../../../models/drive-folder'; import { isValidFolderName } from '../../../models/drive-folder'; import serialize from '../../../serializers/drive-folder'; -import event from '../../../event'; +import { publishDriveStream } from '../../../event'; /** * Create drive folder @@ -52,6 +52,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Response res(folderObj); - // Publish drive_folder_created event - event(user._id, 'drive_folder_created', folderObj); + // Publish folder_created event + publishDriveStream(user._id, 'folder_created', folderObj); }); diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts index cdf055839..a5eb8e015 100644 --- a/src/api/endpoints/drive/folders/find.ts +++ b/src/api/endpoints/drive/folders/find.ts @@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(folders.map(async folder => - await serialize(folder)))); + res(await Promise.all(folders.map(folder => serialize(folder)))); }); diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts index eec275787..ff673402a 100644 --- a/src/api/endpoints/drive/folders/update.ts +++ b/src/api/endpoints/drive/folders/update.ts @@ -4,8 +4,8 @@ import $ from 'cafy'; import DriveFolder from '../../../models/drive-folder'; import { isValidFolderName } from '../../../models/drive-folder'; -import serialize from '../../../serializers/drive-file'; -import event from '../../../event'; +import serialize from '../../../serializers/drive-folder'; +import { publishDriveStream } from '../../../event'; /** * Update a folder @@ -96,6 +96,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // Response res(folderObj); - // Publish drive_folder_updated event - event(user._id, 'drive_folder_updated', folderObj); + // Publish folder_updated event + publishDriveStream(user._id, 'folder_updated', folderObj); }); diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts index 32f7ac7e0..7ee255e5d 100644 --- a/src/api/endpoints/drive/stream.ts +++ b/src/api/endpoints/drive/stream.ts @@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: -1 }; const query = { - user_id: user._id + 'metadata.user_id': user._id } as any; if (sinceId) { sort._id = 1; @@ -52,15 +52,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => { }; } if (type) { - query.type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); + query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`); } // Issue query const files = await DriveFile .find(query, { - fields: { - data: false - }, limit: limit, sort: sort }); diff --git a/src/api/endpoints/i/appdata/get.ts b/src/api/endpoints/i/appdata/get.ts index a1a57fa13..571208d46 100644 --- a/src/api/endpoints/i/appdata/get.ts +++ b/src/api/endpoints/i/appdata/get.ts @@ -13,38 +13,27 @@ import Appdata from '../../../models/appdata'; * @param {Boolean} isSecure * @return {Promise} */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + // Get 'key' parameter const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; if (keyError) return rej('invalid key param'); - if (isSecure) { - if (!user.data) { - return res(); - } - if (key !== null) { - const data = {}; - data[key] = user.data[key]; - res(data); - } else { - res(user.data); - } - } else { - const select = {}; - if (key !== null) { - select[`data.${key}`] = true; - } - const appdata = await Appdata.findOne({ - app_id: app._id, - user_id: user._id - }, { - fields: select - }); + const select = {}; + if (key !== null) { + select[`data.${key}`] = true; + } + const appdata = await Appdata.findOne({ + app_id: app._id, + user_id: user._id + }, { + fields: select + }); - if (appdata) { - res(appdata.data); - } else { - res(); - } + if (appdata) { + res(appdata.data); + } else { + res(); } }); diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts index 9c3dbe185..2804a14cb 100644 --- a/src/api/endpoints/i/appdata/set.ts +++ b/src/api/endpoints/i/appdata/set.ts @@ -3,9 +3,6 @@ */ import $ from 'cafy'; import Appdata from '../../../models/appdata'; -import User from '../../../models/user'; -import serialize from '../../../serializers/user'; -import event from '../../../event'; /** * Set app data @@ -16,7 +13,9 @@ import event from '../../../event'; * @param {Boolean} isSecure * @return {Promise} */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + // Get 'data' parameter const [data, dataError] = $(params.data).optional.object() .pipe(obj => { @@ -43,31 +42,17 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) = set[`data.${key}`] = value; } - if (isSecure) { - const _user = await User.findOneAndUpdate(user._id, { + await Appdata.update({ + app_id: app._id, + user_id: user._id + }, Object.assign({ + app_id: app._id, + user_id: user._id + }, { $set: set + }), { + upsert: true }); - res(204); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(_user, user, { - detail: true, - includeSecrets: true - })); - } else { - await Appdata.update({ - app_id: app._id, - user_id: user._id - }, Object.assign({ - app_id: app._id, - user_id: user._id - }, { - $set: set - }), { - upsert: true - }); - - res(204); - } + res(204); }); diff --git a/src/api/endpoints/i/change_password.ts b/src/api/endpoints/i/change_password.ts index faceded29..16f1a2e4e 100644 --- a/src/api/endpoints/i/change_password.ts +++ b/src/api/endpoints/i/change_password.ts @@ -22,15 +22,15 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (newPasswordErr) return rej('invalid new_password param'); // Compare password - const same = bcrypt.compareSync(currentPassword, user.password); + const same = await bcrypt.compare(currentPassword, user.password); if (!same) { return rej('incorrect password'); } // Generate hash of password - const salt = bcrypt.genSaltSync(8); - const hash = bcrypt.hashSync(newPassword, salt); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(newPassword, salt); await User.update(user._id, { $set: { diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/api/endpoints/i/regenerate_token.ts index f96d10ebf..653468330 100644 --- a/src/api/endpoints/i/regenerate_token.ts +++ b/src/api/endpoints/i/regenerate_token.ts @@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (passwordErr) return rej('invalid password param'); // Compare password - const same = bcrypt.compareSync(password, user.password); + const same = await bcrypt.compare(password, user.password); if (!same) { return rej('incorrect password'); diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts index 111a4b190..c484c51a9 100644 --- a/src/api/endpoints/i/update.ts +++ b/src/api/endpoints/i/update.ts @@ -48,13 +48,19 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re if (bannerIdErr) return rej('invalid banner_id param'); if (bannerId) user.banner_id = bannerId; + // Get 'show_donation' parameter + const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$; + if (showDonationErr) return rej('invalid show_donation param'); + if (showDonation) user.client_settings.show_donation = showDonation; + await User.update(user._id, { $set: { name: user.name, description: user.description, avatar_id: user.avatar_id, banner_id: user.banner_id, - profile: user.profile + profile: user.profile, + 'client_settings.show_donation': user.client_settings.show_donation } }); diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts new file mode 100644 index 000000000..429e88529 --- /dev/null +++ b/src/api/endpoints/i/update_home.ts @@ -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} + */ +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(); + } +}); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts index 8af55d850..3c7689f96 100644 --- a/src/api/endpoints/messaging/messages/create.ts +++ b/src/api/endpoints/messaging/messages/create.ts @@ -9,7 +9,7 @@ import User from '../../../models/user'; import DriveFile from '../../../models/drive-file'; import serialize from '../../../serializers/messaging-message'; import publishUserStream from '../../../event'; -import { publishMessagingStream } from '../../../event'; +import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event'; import config from '../../../../conf'; /** @@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (fileId !== undefined) { file = await DriveFile.findOne({ _id: fileId, - user_id: user._id - }, { - data: false + 'metadata.user_id': user._id }); if (file === null) { @@ -87,10 +85,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => { // 自分のストリーム publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); + publishMessagingIndexStream(message.user_id, 'message', messageObj); publishUserStream(message.user_id, 'messaging_message', messageObj); // 相手のストリーム publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); + publishMessagingIndexStream(message.recipient_id, 'message', messageObj); publishUserStream(message.recipient_id, 'messaging_message', messageObj); // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する @@ -98,6 +98,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); if (!freshMessage.is_read) { publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); + pushSw(message.recipient_id, 'unread_messaging_message', messageObj); } }, 3000); diff --git a/src/api/endpoints/meta.ts b/src/api/endpoints/meta.ts index a3f1d5032..e27ca39e7 100644 --- a/src/api/endpoints/meta.ts +++ b/src/api/endpoints/meta.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import version from '../../version'; import config from '../../conf'; +import Meta from '../models/meta'; /** * @swagger @@ -39,6 +40,8 @@ import config from '../../conf'; * @return {Promise} */ module.exports = (params) => new Promise(async (res, rej) => { + const meta = (await Meta.findOne()) || {}; + res({ maintainer: config.maintainer, version: version, @@ -49,6 +52,8 @@ module.exports = (params) => new Promise(async (res, rej) => { cpu: { model: os.cpus()[0].model, cores: os.cpus().length - } + }, + top_image: meta.top_image, + broadcasts: meta.broadcasts }); }); diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts index f982b9ee9..ae4959dae 100644 --- a/src/api/endpoints/posts/create.ts +++ b/src/api/endpoints/posts/create.ts @@ -14,7 +14,7 @@ import ChannelWatching from '../../models/channel-watching'; import serialize from '../../serializers/post'; import notify from '../../common/notify'; import watch from '../../common/watch-post'; -import { default as event, publishChannelStream } from '../../event'; +import event, { pushSw, publishChannelStream } from '../../event'; import config from '../../../conf'; /** @@ -44,9 +44,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // SELECT _id const entity = await DriveFile.findOne({ _id: mediaId, - user_id: user._id - }, { - _id: true + 'metadata.user_id': user._id }); if (entity === null) { @@ -236,7 +234,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { const mentions = []; - function addMention(mentionee, type) { + function addMention(mentionee, reason) { // Reject if already added if (mentions.some(x => x.equals(mentionee))) return; @@ -245,7 +243,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => { // Publish event if (!user._id.equals(mentionee)) { - event(mentionee, type, postObj); + event(mentionee, reason, postObj); + pushSw(mentionee, reason, postObj); } } diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts index eecb92812..d537463df 100644 --- a/src/api/endpoints/posts/reactions/create.ts +++ b/src/api/endpoints/posts/reactions/create.ts @@ -7,7 +7,9 @@ import Post from '../../../models/post'; import Watching from '../../../models/post-watching'; import notify from '../../../common/notify'; import watch from '../../../common/watch-post'; -import { publishPostStream } from '../../../event'; +import { publishPostStream, pushSw } from '../../../event'; +import serializePost from '../../../serializers/post'; +import serializeUser from '../../../serializers/user'; /** * React to a post @@ -87,6 +89,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => { reaction: reaction }); + pushSw(post.user_id, 'reaction', { + user: await serializeUser(user, post.user_id), + post: await serializePost(post, post.user_id), + reaction: reaction + }); + // Fetch watchers Watching .find({ diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts index aa5aff5ba..0d08b9546 100644 --- a/src/api/endpoints/posts/timeline.ts +++ b/src/api/endpoints/posts/timeline.ts @@ -2,6 +2,7 @@ * Module dependencies */ import $ from 'cafy'; +import rap from '@prezzemolo/rap'; import Post from '../../models/post'; import ChannelWatching from '../../models/channel-watching'; import getFriends from '../../common/get-friends'; @@ -15,32 +16,41 @@ import serialize from '../../serializers/post'; * @param {any} app * @return {Promise} */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { +module.exports = async (params, user, app) => { // Get 'limit' parameter const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); + if (limitErr) throw 'invalid limit param'; // Get 'since_id' parameter const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); + if (sinceIdErr) throw 'invalid since_id param'; // Get 'max_id' parameter const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); + if (maxIdErr) throw 'invalid max_id param'; - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'max_date' parameter + const [maxDate, maxDateErr] = $(params.max_date).optional.number().$; + if (maxDateErr) throw 'invalid max_date param'; + + // Check if only one of since_id, max_id, since_date, max_date specified + if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) { + throw 'only one of since_id, max_id, since_date, max_date can be specified'; } - // ID list of the user itself and other users who the user follows - const followingIds = await getFriends(user._id); - - // Watchしているチャンネルを取得 - const watches = await ChannelWatching.find({ - user_id: user._id, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } + const { followingIds, watchingChannelIds } = await rap({ + // ID list of the user itself and other users who the user follows + followingIds: getFriends(user._id), + // Watchしているチャンネルを取得 + watchingChannelIds: ChannelWatching.find({ + user_id: user._id, + // 削除されたドキュメントは除く + deleted_at: { $exists: false } + }).then(watches => watches.map(w => w.channel_id)) }); //#region Construct query @@ -65,7 +75,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { }, { // Watchしているチャンネルへの投稿 channel_id: { - $in: watches.map(w => w.channel_id) + $in: watchingChannelIds } }] } as any; @@ -79,6 +89,15 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { query._id = { $lt: maxId }; + } else if (sinceDate) { + sort._id = 1; + query.created_at = { + $gt: new Date(sinceDate) + }; + } else if (maxDate) { + query.created_at = { + $lt: new Date(maxDate) + }; } //#endregion @@ -90,7 +109,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { }); // Serialize - res(await Promise.all(timeline.map(async post => - await serialize(post, user) - ))); -}); + return await Promise.all(timeline.map(post => serialize(post, user))); +}; diff --git a/src/api/endpoints/sw/register.ts b/src/api/endpoints/sw/register.ts new file mode 100644 index 000000000..99406138d --- /dev/null +++ b/src/api/endpoints/sw/register.ts @@ -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} + */ +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(); +}); diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts index bb0f3b4ce..a8add623d 100644 --- a/src/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/api/endpoints/users/get_frequently_replied_users.ts @@ -11,6 +11,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [userId, userIdErr] = $(params.user_id).id().$; if (userIdErr) return rej('invalid user_id param'); + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) return rej('invalid limit param'); + // Lookup user const user = await User.findOne({ _id: userId @@ -82,8 +86,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { // Sort replies by frequency const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); - // Lookup top 10 replies - const topRepliedUsers = repliedUsersSorted.slice(0, 10); + // Extract top replied users + const topRepliedUsers = repliedUsersSorted.slice(0, limit); // Make replies object (includes weights) const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts index d8204b8b8..fe821cf17 100644 --- a/src/api/endpoints/users/posts.ts +++ b/src/api/endpoints/users/posts.ts @@ -46,9 +46,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [maxId, maxIdErr] = $(params.max_id).optional.id().$; if (maxIdErr) return rej('invalid max_id param'); - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); + // Get 'since_date' parameter + const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$; + if (sinceDateErr) throw 'invalid since_date param'; + + // Get 'max_date' parameter + const [maxDate, maxDateErr] = $(params.max_date).optional.number().$; + if (maxDateErr) throw 'invalid max_date param'; + + // Check if only one of since_id, max_id, since_date, max_date specified + if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) { + throw 'only one of since_id, max_id, since_date, max_date can be specified'; } const q = userId !== undefined @@ -66,13 +74,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { return rej('user not found'); } - // Construct query + //#region Construct query const sort = { _id: -1 }; + const query = { user_id: user._id } as any; + if (sinceId) { sort._id = 1; query._id = { @@ -82,6 +92,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { query._id = { $lt: maxId }; + } else if (sinceDate) { + sort._id = 1; + query.created_at = { + $gt: new Date(sinceDate) + }; + } else if (maxDate) { + query.created_at = { + $lt: new Date(maxDate) + }; } if (!includeReplies) { @@ -94,6 +113,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { $ne: null }; } + //#endregion // Issue query const posts = await Post diff --git a/src/api/event.ts b/src/api/event.ts index 909b0d255..4a2e4e453 100644 --- a/src/api/event.ts +++ b/src/api/event.ts @@ -1,5 +1,6 @@ import * as mongo from 'mongodb'; import * as redis from 'redis'; +import swPush from './common/push-sw'; import config from '../conf'; type ID = string | mongo.ObjectID; @@ -17,6 +18,14 @@ class MisskeyEvent { this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); } + public publishSw(userId: ID, type: string, value?: any): void { + swPush(userId, type, value); + } + + public publishDriveStream(userId: ID, type: string, value?: any): void { + this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + public publishPostStream(postId: ID, type: string, value?: any): void { this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value); } @@ -25,6 +34,10 @@ class MisskeyEvent { this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } + public publishMessagingIndexStream(userId: ID, type: string, value?: any): void { + this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + public publishChannelStream(channelId: ID, type: string, value?: any): void { this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value); } @@ -42,8 +55,14 @@ const ev = new MisskeyEvent(); export default ev.publishUserStream.bind(ev); +export const pushSw = ev.publishSw.bind(ev); + +export const publishDriveStream = ev.publishDriveStream.bind(ev); + export const publishPostStream = ev.publishPostStream.bind(ev); export const publishMessagingStream = ev.publishMessagingStream.bind(ev); +export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); + export const publishChannelStream = ev.publishChannelStream.bind(ev); diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts index 8d158cf56..802ee5a5f 100644 --- a/src/api/models/drive-file.ts +++ b/src/api/models/drive-file.ts @@ -1,11 +1,20 @@ -import db from '../../db/mongodb'; +import * as mongodb from 'mongodb'; +import monkDb, { nativeDbConn } from '../../db/mongodb'; -const collection = db.get('drive_files'); - -(collection as any).createIndex('hash'); // fuck type definition +const collection = monkDb.get('drive_files.files'); export default collection as any; // fuck type definition +const getGridFSBucket = async (): Promise => { + const db = await nativeDbConn(); + const bucket = new mongodb.GridFSBucket(db, { + bucketName: 'drive_files' + }); + return bucket; +}; + +export { getGridFSBucket }; + export function validateFileName(name: string): boolean { return ( (name.trim().length > 0) && diff --git a/src/api/models/meta.ts b/src/api/models/meta.ts new file mode 100644 index 000000000..c7dba8fcb --- /dev/null +++ b/src/api/models/meta.ts @@ -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; +}; diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts index 1065e8baa..e3dc6c70a 100644 --- a/src/api/models/notification.ts +++ b/src/api/models/notification.ts @@ -1,8 +1,47 @@ import * as mongo from 'mongodb'; import db from '../../db/mongodb'; +import { IUser } from './user'; export default db.get('notifications') as any; // fuck type definition export interface INotification { _id: mongo.ObjectID; + created_at: Date; + + /** + * 通知の受信者 + */ + notifiee?: IUser; + + /** + * 通知の受信者 + */ + notifiee_id: mongo.ObjectID; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifier?: IUser; + + /** + * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー + */ + notifier_id: mongo.ObjectID; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - (自分または自分がWatchしている)投稿が返信された + * repost - (自分または自分がWatchしている)投稿がRepostされた + * quote - (自分または自分がWatchしている)投稿が引用Repostされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された + */ + type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote'; + + /** + * 通知が読まれたかどうか + */ + is_read: Boolean; } diff --git a/src/api/models/sw-subscription.ts b/src/api/models/sw-subscription.ts new file mode 100644 index 000000000..ecca04cb9 --- /dev/null +++ b/src/api/models/sw-subscription.ts @@ -0,0 +1,3 @@ +import db from '../../db/mongodb'; + +export default db.get('sw_subscriptions') as any; // fuck type definition diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts index c7dc24398..0ebf8d6aa 100644 --- a/src/api/private/signin.ts +++ b/src/api/private/signin.ts @@ -4,7 +4,7 @@ import { default as User, IUser } from '../models/user'; import Signin from '../models/signin'; import serialize from '../serializers/signin'; import event from '../event'; -import config from '../../conf'; +import signin from '../common/signin'; export default async (req: express.Request, res: express.Response) => { res.header('Access-Control-Allow-Credentials', 'true'); @@ -40,20 +40,10 @@ export default async (req: express.Request, res: express.Response) => { } // Compare password - const same = bcrypt.compareSync(password, user.password); + const same = await bcrypt.compare(password, user.password); if (same) { - const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.token, { - path: '/', - domain: `.${config.host}`, - secure: config.url.substr(0, 5) === 'https', - httpOnly: false, - expires: new Date(Date.now() + expires), - maxAge: expires - }); - - res.sendStatus(204); + signin(res, user, false); } else { res.status(400).send({ error: 'incorrect password' diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts index bcc17a876..466c6a489 100644 --- a/src/api/private/signup.ts +++ b/src/api/private/signup.ts @@ -1,3 +1,4 @@ +import * as uuid from 'uuid'; import * as express from 'express'; import * as bcrypt from 'bcryptjs'; import recaptcha = require('recaptcha-promise'); @@ -8,9 +9,31 @@ import generateUserToken from '../common/generate-native-user-token'; import config from '../../conf'; recaptcha.init({ - secret_key: config.recaptcha.secretKey + secret_key: config.recaptcha.secret_key }); +const home = { + left: [ + 'profile', + 'calendar', + 'activity', + 'rss-reader', + 'trends', + 'photo-stream', + 'version' + ], + right: [ + 'broadcast', + 'notifications', + 'user-recommendation', + 'recommended-polls', + 'server', + 'donation', + 'nav', + 'tips' + ] +}; + export default async (req: express.Request, res: express.Response) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする @@ -54,12 +77,34 @@ export default async (req: express.Request, res: express.Response) => { } // Generate hash of password - const salt = bcrypt.genSaltSync(8); - const hash = bcrypt.hashSync(password, salt); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); // Generate secret const secret = generateUserToken(); + //#region Construct home data + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left', + data: {} + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right', + data: {} + }); + }); + //#endregion + // Create account const account: IUser = await User.insert({ token: secret, @@ -88,6 +133,11 @@ export default async (req: express.Request, res: express.Response) => { height: null, location: null, weight: null + }, + settings: {}, + client_settings: { + home: homeData, + show_donation: false } }); diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts index b4e2ab064..dcdaa01fa 100644 --- a/src/api/serializers/drive-file.ts +++ b/src/api/serializers/drive-file.ts @@ -31,44 +31,44 @@ export default ( if (mongo.ObjectID.prototype.isPrototypeOf(file)) { _file = await DriveFile.findOne({ _id: file - }, { - fields: { - data: false - } - }); + }); } else if (typeof file === 'string') { _file = await DriveFile.findOne({ _id: new mongo.ObjectID(file) - }, { - fields: { - data: false - } - }); + }); } else { _file = deepcopy(file); } - // Rename _id to id - _file.id = _file._id; - delete _file._id; + if (!_file) return reject('invalid file arg.'); - delete _file.data; + // rendered target + let _target: any = {}; - _file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`; + _target.id = _file._id; + _target.created_at = _file.uploadDate; + _target.name = _file.filename; + _target.type = _file.contentType; + _target.datasize = _file.length; + _target.md5 = _file.md5; - if (opts.detail && _file.folder_id) { + _target = Object.assign(_target, _file.metadata); + + _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`; + + if (opts.detail && _target.folder_id) { // Populate folder - _file.folder = await serializeDriveFolder(_file.folder_id, { + _target.folder = await serializeDriveFolder(_target.folder_id, { detail: true }); } - if (opts.detail && _file.tags) { + if (opts.detail && _target.tags) { // Populate tags - _file.tags = await _file.tags.map(async (tag: any) => + _target.tags = await _target.tags.map(async (tag: any) => await serializeDriveTag(tag) ); } - resolve(_file); + resolve(_target); }); diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts index a42846410..6ebf454a2 100644 --- a/src/api/serializers/drive-folder.ts +++ b/src/api/serializers/drive-folder.ts @@ -44,7 +44,7 @@ const self = ( }); const childFilesCount = await DriveFile.count({ - folder_id: _folder.id + 'metadata.folder_id': _folder.id }); _folder.folders_count = childFoldersCount; diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts index 7c3690ef7..03fd12077 100644 --- a/src/api/serializers/post.ts +++ b/src/api/serializers/post.ts @@ -12,6 +12,7 @@ import serializeChannel from './channel'; import serializeUser from './user'; import serializeDriveFile from './drive-file'; import parse from '../common/text'; +import rap from '@prezzemolo/rap'; /** * Serialize a post @@ -21,13 +22,13 @@ import parse from '../common/text'; * @param options? serialize options * @return response */ -const self = ( +const self = async ( post: string | mongo.ObjectID | IPost, me?: string | mongo.ObjectID | IUser, options?: { detail: boolean } -) => new Promise(async (resolve, reject) => { +) => { const opts = options || { detail: true, }; @@ -56,6 +57,8 @@ const self = ( _post = deepcopy(post); } + if (!_post) throw 'invalid post arg.'; + const id = _post._id; // Rename _id to id @@ -70,105 +73,120 @@ const self = ( } // Populate user - _post.user = await serializeUser(_post.user_id, meId); + _post.user = serializeUser(_post.user_id, meId); // Populate app if (_post.app_id) { - _post.app = await serializeApp(_post.app_id); + _post.app = serializeApp(_post.app_id); } // Populate channel if (_post.channel_id) { - _post.channel = await serializeChannel(_post.channel_id); + _post.channel = serializeChannel(_post.channel_id); } // Populate media if (_post.media_ids) { - _post.media = await Promise.all(_post.media_ids.map(async fileId => - await serializeDriveFile(fileId) + _post.media = Promise.all(_post.media_ids.map(fileId => + serializeDriveFile(fileId) )); } // When requested a detailed post data if (opts.detail) { // Get previous post info - const prev = await Post.findOne({ - user_id: _post.user_id, - _id: { - $lt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: -1 - } - }); - _post.prev = prev ? prev._id : null; + _post.prev = (async () => { + const prev = await Post.findOne({ + user_id: _post.user_id, + _id: { + $lt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: -1 + } + }); + return prev ? prev._id : null; + })(); // Get next post info - const next = await Post.findOne({ - user_id: _post.user_id, - _id: { - $gt: id - } - }, { - fields: { - _id: true - }, - sort: { - _id: 1 - } - }); - _post.next = next ? next._id : null; + _post.next = (async () => { + const next = await Post.findOne({ + user_id: _post.user_id, + _id: { + $gt: id + } + }, { + fields: { + _id: true + }, + sort: { + _id: 1 + } + }); + return next ? next._id : null; + })(); if (_post.reply_id) { // Populate reply to post - _post.reply = await self(_post.reply_id, meId, { + _post.reply = self(_post.reply_id, meId, { detail: false }); } if (_post.repost_id) { // Populate repost - _post.repost = await self(_post.repost_id, meId, { + _post.repost = self(_post.repost_id, meId, { detail: _post.text == null }); } // Poll if (meId && _post.poll) { - const vote = await Vote - .findOne({ - user_id: meId, - post_id: id - }); + _post.poll = (async (poll) => { + const vote = await Vote + .findOne({ + user_id: meId, + post_id: id + }); - if (vote != null) { - const myChoice = _post.poll.choices - .filter(c => c.id == vote.choice)[0]; + if (vote != null) { + const myChoice = poll.choices + .filter(c => c.id == vote.choice)[0]; - myChoice.is_voted = true; - } + myChoice.is_voted = true; + } + + return poll; + })(_post.poll); } // Fetch my reaction if (meId) { - const reaction = await Reaction - .findOne({ - user_id: meId, - post_id: id, - deleted_at: { $exists: false } - }); + _post.my_reaction = (async () => { + const reaction = await Reaction + .findOne({ + user_id: meId, + post_id: id, + deleted_at: { $exists: false } + }); - if (reaction) { - _post.my_reaction = reaction.reaction; - } + if (reaction) { + return reaction.reaction; + } + + return null; + })(); } } - resolve(_post); -}); + // resolve promises in _post object + _post = await rap(_post); + + return _post; +}; export default self; diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts index 3deff2d00..3d8415660 100644 --- a/src/api/serializers/user.ts +++ b/src/api/serializers/user.ts @@ -8,6 +8,7 @@ import serializePost from './post'; import Following from '../models/following'; import getFriends from '../common/get-friends'; import config from '../../conf'; +import rap from '@prezzemolo/rap'; /** * Serialize a user @@ -34,9 +35,10 @@ export default ( let _user: any; const fields = opts.detail ? { - data: false + settings: false } : { - data: false, + settings: false, + client_settings: false, profile: false, keywords: false, domains: false @@ -55,6 +57,8 @@ export default ( _user = deepcopy(user); } + if (!_user) return reject('invalid user arg.'); + // Me const meId: mongo.ObjectID = me ? mongo.ObjectID.prototype.isPrototypeOf(me) @@ -69,7 +73,7 @@ export default ( delete _user._id; // Remove needless properties - delete _user.lates_post; + delete _user.latest_post; // Remove private properties delete _user.password; @@ -83,8 +87,8 @@ export default ( // Visible via only the official client if (!opts.includeSecrets) { - delete _user.data; delete _user.email; + delete _user.client_settings; } _user.avatar_url = _user.avatar_id != null @@ -104,26 +108,30 @@ export default ( if (meId && !meId.equals(_user.id)) { // If the user is following - const follow = await Following.findOne({ - follower_id: meId, - followee_id: _user.id, - deleted_at: { $exists: false } - }); - _user.is_following = follow !== null; + _user.is_following = (async () => { + const follow = await Following.findOne({ + follower_id: meId, + followee_id: _user.id, + deleted_at: { $exists: false } + }); + return follow !== null; + })(); // If the user is followed - const follow2 = await Following.findOne({ - follower_id: _user.id, - followee_id: meId, - deleted_at: { $exists: false } - }); - _user.is_followed = follow2 !== null; + _user.is_followed = (async () => { + const follow2 = await Following.findOne({ + follower_id: _user.id, + followee_id: meId, + deleted_at: { $exists: false } + }); + return follow2 !== null; + })(); } if (opts.detail) { if (_user.pinned_post_id) { // Populate pinned post - _user.pinned_post = await serializePost(_user.pinned_post_id, meId, { + _user.pinned_post = serializePost(_user.pinned_post_id, meId, { detail: true }); } @@ -132,23 +140,24 @@ export default ( const myFollowingIds = await getFriends(meId); // Get following you know count - const followingYouKnowCount = await Following.count({ + _user.following_you_know_count = Following.count({ followee_id: { $in: myFollowingIds }, follower_id: _user.id, deleted_at: { $exists: false } }); - _user.following_you_know_count = followingYouKnowCount; // Get followers you know count - const followersYouKnowCount = await Following.count({ + _user.followers_you_know_count = Following.count({ followee_id: _user.id, follower_id: { $in: myFollowingIds }, deleted_at: { $exists: false } }); - _user.followers_you_know_count = followersYouKnowCount; } } + // resolve promises in _user object + _user = await rap(_user); + resolve(_user); }); /* diff --git a/src/api/server.ts b/src/api/server.ts index 3de32d9ea..026357b46 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -40,7 +40,7 @@ app.get('/', (req, res) => { endpoints.forEach(endpoint => endpoint.withFile ? app.post(`/${endpoint.name}`, - endpoint.withFile ? multer({ dest: 'uploads/' }).single('file') : null, + endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null, require('./api-handler').default.bind(null, endpoint)) : app.post(`/${endpoint.name}`, require('./api-handler').default.bind(null, endpoint)) diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts index 9fb274aac..f164cdc45 100644 --- a/src/api/service/twitter.ts +++ b/src/api/service/twitter.ts @@ -1,4 +1,6 @@ import * as express from 'express'; +import * as cookie from 'cookie'; +import * as uuid from 'uuid'; // import * as Twitter from 'twitter'; // const Twitter = require('twitter'); import autwh from 'autwh'; @@ -7,6 +9,7 @@ import User from '../models/user'; import serialize from '../serializers/user'; import event from '../event'; import config from '../../conf'; +import signin from '../common/signin'; module.exports = (app: express.Application) => { app.get('/disconnect/twitter', async (req, res): Promise => { @@ -30,8 +33,13 @@ module.exports = (app: express.Application) => { if (config.twitter == null) { app.get('/connect/twitter', (req, res) => { - res.send('現在Twitterへ接続できません'); + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); }); + + app.get('/signin/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + return; } @@ -48,14 +56,58 @@ module.exports = (app: express.Application) => { res.redirect(ctx.url); }); - app.get('/tw/cb', (req, res): any => { - if (res.locals.user == null) return res.send('plz signin'); - redis.get(res.locals.user, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + app.get('/signin/twitter', async (req, res): Promise => { + const ctx = await twAuth.begin(); - const user = await User.findOneAndUpdate({ - token: res.locals.user - }, { + const sessid = uuid(); + + redis.set(sessid, JSON.stringify(ctx)); + + const expires = 1000 * 60 * 60; // 1h + res.cookie('signin_with_twitter_session_id', sessid, { + path: '/', + domain: `.${config.host}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + res.redirect(ctx.url); + }); + + app.get('/tw/cb', (req, res): any => { + if (res.locals.user == null) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const cookies = cookie.parse((req.headers['cookie'] as string || '')); + + const sessid = cookies['signin_with_twitter_session_id']; + + if (sessid == undefined) { + res.status(400).send('invalid session'); + } + + redis.get(sessid, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + + const user = await User.findOne({ + 'twitter.user_id': result.userId + }); + + if (user == null) { + res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + } + + signin(res, user, true); + }); + } else { + redis.get(res.locals.user, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + + const user = await User.findOneAndUpdate({ + token: res.locals.user + }, { $set: { twitter: { access_token: result.accessToken, @@ -66,13 +118,14 @@ module.exports = (app: express.Application) => { } }); - res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); - // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { - detail: true, - includeSecrets: true - })); - }); + // Publish i updated event + event(user._id, 'i_updated', await serialize(user, user, { + detail: true, + includeSecrets: true + })); + }); + } }); }; diff --git a/src/api/stream/drive.ts b/src/api/stream/drive.ts new file mode 100644 index 000000000..c97ab80dc --- /dev/null +++ b/src/api/stream/drive.ts @@ -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); + }); +} diff --git a/src/api/stream/messaging-index.ts b/src/api/stream/messaging-index.ts new file mode 100644 index 000000000..c1b2fbc80 --- /dev/null +++ b/src/api/stream/messaging-index.ts @@ -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); + }); +} diff --git a/src/api/stream/requests.ts b/src/api/stream/requests.ts new file mode 100644 index 000000000..2c36e58b6 --- /dev/null +++ b/src/api/stream/requests.ts @@ -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); + }); +} diff --git a/src/api/streaming.ts b/src/api/streaming.ts index 0e512fb21..c06d64c24 100644 --- a/src/api/streaming.ts +++ b/src/api/streaming.ts @@ -7,8 +7,11 @@ import AccessToken from './models/access-token'; import isNativeToken from './common/is-native-token'; import homeStream from './stream/home'; +import driveStream from './stream/drive'; import messagingStream from './stream/messaging'; +import messagingIndexStream from './stream/messaging-index'; import serverStream from './stream/server'; +import requestsStream from './stream/requests'; import channelStream from './stream/channel'; module.exports = (server: http.Server) => { @@ -27,6 +30,11 @@ module.exports = (server: http.Server) => { return; } + if (request.resourceURL.pathname === '/requests') { + requestsStream(request, connection); + return; + } + // Connect to Redis const subscriber = redis.createClient( config.redis.port, config.redis.host); @@ -51,7 +59,9 @@ module.exports = (server: http.Server) => { const channel = request.resourceURL.pathname === '/' ? homeStream : + request.resourceURL.pathname === '/drive' ? driveStream : request.resourceURL.pathname === '/messaging' ? messagingStream : + request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : null; if (channel !== null) { diff --git a/src/common/get-notification-summary.ts b/src/common/get-notification-summary.ts new file mode 100644 index 000000000..03db722c8 --- /dev/null +++ b/src/common/get-notification-summary.ts @@ -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}>`; + } +} diff --git a/src/common/get-reaction-emoji.ts b/src/common/get-reaction-emoji.ts new file mode 100644 index 000000000..c66120537 --- /dev/null +++ b/src/common/get-reaction-emoji.ts @@ -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 ''; + } +} diff --git a/src/config.ts b/src/config.ts index d37d227a4..3ff800758 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,6 @@ */ import * as fs from 'fs'; -import * as URL from 'url'; import * as yaml from 'js-yaml'; import isUrl = require('is-url'); @@ -23,16 +22,23 @@ export const path = process.env.NODE_ENV == 'test' * ユーザーが設定する必要のある情報 */ type Source = { - maintainer: string; + /** + * メンテナ情報 + */ + maintainer: { + /** + * メンテナの名前 + */ + name: string; + /** + * メンテナの連絡先(URLかmailto形式のURL) + */ + url: string; + }; url: string; secondary_url: string; port: number; - https: { - enable: boolean; - key: string; - cert: string; - ca: string; - }; + https?: { [x: string]: string }; mongodb: { host: string; port: number; @@ -52,8 +58,8 @@ type Source = { pass: string; }; recaptcha: { - siteKey: string; - secretKey: string; + site_key: string; + secret_key: string; }; accesslog?: string; accesses?: { @@ -75,6 +81,14 @@ type Source = { analysis?: { mecab_command?: string; }; + + /** + * Service Worker + */ + sw?: { + public_key: string; + private_key: string; + }; }; /** @@ -106,14 +120,6 @@ export default function load() { if (!isUrl(config.url)) urlError(config.url); if (!isUrl(config.secondary_url)) urlError(config.secondary_url); - const url = URL.parse(config.url); - const head = url.host.split('.')[0]; - - if (head != 'misskey') { - console.error(`プライマリドメインは、必ず「misskey」ドメインで始まっていなければなりません(現在の設定では「${head}」で始まっています)。例えば「https://misskey.xyz」「http://misskey.my.app.example.com」などが正しいプライマリURLです。`); - process.exit(); - } - config.url = normalizeUrl(config.url); config.secondary_url = normalizeUrl(config.secondary_url); diff --git a/src/const.json b/src/const.json index eeb304c9f..924b4dd8b 100644 --- a/src/const.json +++ b/src/const.json @@ -1,4 +1,4 @@ { - "themeColor": "#f43636", + "themeColor": "#ff4e45", "themeColorForeground": "#fff" } diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts index 6ee7f4534..c978e6460 100644 --- a/src/db/mongodb.ts +++ b/src/db/mongodb.ts @@ -1,11 +1,38 @@ -import * as mongo from 'monk'; - import config from '../conf'; const uri = config.mongodb.user && config.mongodb.pass - ? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` - : `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; +? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}` +: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; + +/** + * monk + */ +import * as mongo from 'monk'; const db = mongo(uri); export default db; + +/** + * MongoDB native module (officialy) + */ +import * as mongodb from 'mongodb'; + +let mdb: mongodb.Db; + +const nativeDbConn = async (): Promise => { + if (mdb) return mdb; + + const db = await ((): Promise => new Promise((resolve, reject) => { + mongodb.MongoClient.connect(uri, (e, db) => { + if (e) return reject(e); + resolve(db); + }); + }))(); + + mdb = db; + + return db; +}; + +export { nativeDbConn }; diff --git a/src/file/assets/not-an-image.png b/src/file/assets/not-an-image.png new file mode 100644 index 000000000..cfbc04fcc --- /dev/null +++ b/src/file/assets/not-an-image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc2bc9656bc98009e2e7b58959a4ce2a0ecbd3ec00519118e8b2a0eca0356d9c +size 4711 diff --git a/src/file/assets/thumbnail-not-available.png b/src/file/assets/thumbnail-not-available.png new file mode 100644 index 000000000..c593c0c5f --- /dev/null +++ b/src/file/assets/thumbnail-not-available.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5583e353d1a248875145b8148c4ff7312589801fc3d97ad22ebdc5ffd0ba5251 +size 8822 diff --git a/src/file/server.ts b/src/file/server.ts index ee67cf786..1f8d21b80 100644 --- a/src/file/server.ts +++ b/src/file/server.ts @@ -8,8 +8,9 @@ import * as bodyParser from 'body-parser'; import * as cors from 'cors'; import * as mongodb from 'mongodb'; import * as gm from 'gm'; +import * as stream from 'stream'; -import File from '../api/models/drive-file'; +import DriveFile, { getGridFSBucket } from '../api/models/drive-file'; /** * Init app @@ -33,101 +34,127 @@ app.get('/', (req, res) => { }); app.get('/default-avatar.jpg', (req, res) => { - const file = fs.readFileSync(`${__dirname}/assets/avatar.jpg`); + const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); send(file, 'image/jpeg', req, res); }); app.get('/app-default.jpg', (req, res) => { - const file = fs.readFileSync(`${__dirname}/assets/dummy.png`); + const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); send(file, 'image/png', req, res); }); -async function raw(data: Buffer, type: string, download: boolean, res: express.Response): Promise { - res.header('Content-Type', type); - - if (download) { - res.header('Content-Disposition', 'attachment'); - } - - res.send(data); +interface ISend { + contentType: string; + stream: stream.Readable; } -async function thumbnail(data: Buffer, type: string, resize: number, res: express.Response): Promise { - if (!/^image\/.*$/.test(type)) { - data = fs.readFileSync(`${__dirname}/assets/dummy.png`); - } +function thumbnail(data: stream.Readable, type: string, resize: number): ISend { + const readable: stream.Readable = (() => { + // 画像ではない場合 + if (!/^image\/.*$/.test(type)) { + // 使わないことにしたストリームはしっかり取り壊しておく + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); + } - let g = gm(data); + const imageType = type.split('/')[1]; + + // 画像でもPNGかJPEGでないならダメ + if (imageType != 'png' && imageType != 'jpeg') { + // 使わないことにしたストリームはしっかり取り壊しておく + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + } + + return data; + })(); + + let g = gm(readable); if (resize) { g = g.resize(resize, resize); } - g + const stream = g .compress('jpeg') .quality(80) - .toBuffer('jpeg', (err, img) => { - if (err !== undefined && err !== null) { - console.error(err); - res.sendStatus(500); - return; - } + .stream(); - res.header('Content-Type', 'image/jpeg'); - res.send(img); - }); + return { + contentType: 'image/jpeg', + stream + }; } -function send(data: Buffer, type: string, req: express.Request, res: express.Response): void { - if (req.query.thumbnail !== undefined) { - thumbnail(data, type, req.query.size, res); - } else { - raw(data, type, req.query.download !== undefined, res); +const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => { + console.dir(e); + req.destroy(); + res.destroy(e); +}; + +function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void { + readable.on('error', commonReadableHandlerGenerator(req, res)); + + const data = ((): ISend => { + if (req.query.thumbnail !== undefined) { + return thumbnail(readable, type, req.query.size); + } + return { + contentType: type, + stream: readable + }; + })(); + + if (readable !== data.stream) { + data.stream.on('error', commonReadableHandlerGenerator(req, res)); } + + if (req.query.download !== undefined) { + res.header('Content-Disposition', 'attachment'); + } + + res.header('Content-Type', data.contentType); + + data.stream.pipe(res); + + data.stream.on('end', () => { + res.end(); + }); +} + +async function sendFileById(req: express.Request, res: express.Response): Promise { + // Validate id + if (!mongodb.ObjectID.isValid(req.params.id)) { + res.status(400).send('incorrect id'); + return; + } + + const fileId = new mongodb.ObjectID(req.params.id); + const file = await DriveFile.findOne({ _id: fileId }); + + // validate name + if (req.params.name !== undefined && req.params.name !== file.filename) { + res.status(404).send('there is no file has given name'); + return; + } + + if (file == null) { + res.status(404).sendFile(`${__dirname}/assets/dummy.png`); + return; + } + + const bucket = await getGridFSBucket(); + + const readable = bucket.openDownloadStream(fileId); + + send(readable, file.contentType, req, res); } /** * Routing */ -app.get('/:id', async (req, res) => { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); - - if (file == null) { - res.status(404).sendFile(`${__dirname} / assets / dummy.png`); - return; - } else if (file.data == null) { - res.sendStatus(400); - return; - } - - send(file.data.buffer, file.type, req, res); -}); - -app.get('/:id/:name', async (req, res) => { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) }); - - if (file == null) { - res.status(404).sendFile(`${__dirname}/assets/dummy.png`); - return; - } else if (file.data == null) { - res.sendStatus(400); - return; - } - - send(file.data.buffer, file.type, req, res); -}); +app.get('/:id', sendFileById); +app.get('/:id/:name', sendFileById); module.exports = app; diff --git a/src/index.ts b/src/index.ts index aa53c9123..218455d6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as cluster from 'cluster'; import * as debug from 'debug'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; // import portUsed = require('tcp-port-used'); import isRoot = require('is-root'); import { master } from 'accesses'; diff --git a/src/log-request.ts b/src/log-request.ts new file mode 100644 index 000000000..e431aa271 --- /dev/null +++ b/src/log-request.ts @@ -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 + }); +} diff --git a/src/server.ts b/src/server.ts index 240800c1e..a2165d672 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,6 +11,7 @@ import * as morgan from 'morgan'; import Accesses from 'accesses'; import vhost = require('vhost'); +import log from './log-request'; import config from './conf'; /** @@ -35,7 +36,12 @@ app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null })); -// Drop request that without 'Host' header +app.use((req, res, next) => { + log(req); + next(); +}); + +// Drop request when without 'Host' header app.use((req, res, next) => { if (!req.headers['host']) { res.sendStatus(400); @@ -55,13 +61,17 @@ app.use(require('./web/server')); /** * Create server */ -const server = config.https.enable ? - https.createServer({ - key: fs.readFileSync(config.https.key), - cert: fs.readFileSync(config.https.cert), - ca: fs.readFileSync(config.https.ca) - }, app) : - http.createServer(app); +const server = (() => { + if (config.https) { + const certs = {}; + Object.keys(config.https).forEach(k => { + certs[k] = fs.readFileSync(config.https[k]); + }); + return https.createServer(certs, app); + } else { + return http.createServer(app); + } +})(); /** * Steaming diff --git a/src/utils/cli/progressbar.ts b/src/utils/cli/progressbar.ts index 4afb4b090..72496fded 100644 --- a/src/utils/cli/progressbar.ts +++ b/src/utils/cli/progressbar.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import * as readline from 'readline'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; /** * Progress bar diff --git a/src/utils/logger.ts b/src/utils/logger.ts index ecfacbc95..fae1042c3 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,8 +1,8 @@ -import * as chalk from 'chalk'; +import chalk, { Chalk } from 'chalk'; export type LogLevel = 'Error' | 'Warn' | 'Info'; -function toLevelColor(level: LogLevel): chalk.ChalkStyle { +function toLevelColor(level: LogLevel): Chalk { switch (level) { case 'Error': return chalk.red; case 'Warn': return chalk.yellow; diff --git a/src/web/app/auth/script.js b/src/web/app/auth/script.ts similarity index 96% rename from src/web/app/auth/script.js rename to src/web/app/auth/script.ts index fe7f9befe..dd598d1ed 100644 --- a/src/web/app/auth/script.js +++ b/src/web/app/auth/script.ts @@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携'; /** * init */ -init(me => { +init(() => { mount(document.createElement('mk-index')); }); diff --git a/src/web/app/auth/tags/index.js b/src/web/app/auth/tags/index.ts similarity index 100% rename from src/web/app/auth/tags/index.js rename to src/web/app/auth/tags/index.ts diff --git a/src/web/app/base.pug b/src/web/app/base.pug index b1ca80deb..3c3546d50 100644 --- a/src/web/app/base.pug +++ b/src/web/app/base.pug @@ -9,6 +9,7 @@ html meta(name='application-name' content='Misskey') meta(name='theme-color' content=themeColor) meta(name='referrer' content='origin') + link(rel='manifest' href='/manifest.json') title Misskey diff --git a/src/web/app/boot.js b/src/web/app/boot.js index ac6c18d64..4a8ea030a 100644 --- a/src/web/app/boot.js +++ b/src/web/app/boot.js @@ -27,7 +27,9 @@ // misskey.alice => misskey // misskey.strawberry.pasta => misskey // dev.misskey.arisu.tachibana => dev - let app = url.host.split('.')[0]; + let app = url.host == 'localhost' + ? 'misskey' + : url.host.split('.')[0]; // Detect the user language // Note: The default language is English diff --git a/src/web/app/ch/router.js b/src/web/app/ch/router.ts similarity index 88% rename from src/web/app/ch/router.js rename to src/web/app/ch/router.ts index 424158f40..f10c4acdf 100644 --- a/src/web/app/ch/router.js +++ b/src/web/app/ch/router.ts @@ -1,8 +1,8 @@ import * as riot from 'riot'; -const route = require('page'); +import * as route from 'page'; let page = null; -export default me => { +export default () => { route('/', index); route('/:channel', channel); route('*', notFound); @@ -22,7 +22,7 @@ export default me => { } // EXEC - route(); + (route as any)(); }; function mount(content) { diff --git a/src/web/app/ch/script.js b/src/web/app/ch/script.ts similarity index 87% rename from src/web/app/ch/script.js rename to src/web/app/ch/script.ts index 760d405c5..e23558037 100644 --- a/src/web/app/ch/script.js +++ b/src/web/app/ch/script.ts @@ -12,7 +12,7 @@ import route from './router'; /** * init */ -init(me => { +init(() => { // Start routing - route(me); + route(); }); diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag index 4ae62e7b3..716d61cde 100644 --- a/src/web/app/ch/tags/channel.tag +++ b/src/web/app/ch/tags/channel.tag @@ -26,11 +26,11 @@
-

参加するにはログインまたは新規登録してください

+

参加するにはログインまたは新規登録してください


- Misskey ver { version } (葵 aoi) + Misskey ver { _VERSION_ } (葵 aoi)
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag index 9c9674624..b9bd85985 100644 --- a/src/web/app/common/tags/signin-history.tag +++ b/src/web/app/common/tags/signin-history.tag @@ -50,7 +50,10 @@ diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag index 470426700..3b70505ba 100644 --- a/src/web/app/common/tags/twitter-setting.tag +++ b/src/web/app/common/tags/twitter-setting.tag @@ -1,10 +1,10 @@ -

%i18n:common.tags.mk-twitter-setting.description%%i18n:common.tags.mk-twitter-setting.detail%

+

%i18n:common.tags.mk-twitter-setting.description%%i18n:common.tags.mk-twitter-setting.detail%

- { I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' } + { I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' } or - %i18n:common.tags.mk-twitter-setting.disconnect% + %i18n:common.tags.mk-twitter-setting.disconnect%

Twitter ID: { I.twitter.user_id }

+ diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag index 8bd8bfb2a..2274e8416 100644 --- a/src/web/app/desktop/tags/home-widgets/activity.tag +++ b/src/web/app/desktop/tags/home-widgets/activity.tag @@ -1,234 +1,32 @@ -

%i18n:desktop.tags.mk-activity-home-widget.title%

- -

%i18n:common.loading%

- - +
- - - - - { date.year }/{ date.month }/{ date.day }
Post: { posts }, Reply: { replies }, Repost: { reposts }
-
- - -
- - -
- - - - Black ... Total
Blue ... Posts
Red ... Replies
Green ... Reposts
- - - - -
- - -
- diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag index 1102e22c7..6f4bb0756 100644 --- a/src/web/app/desktop/tags/home-widgets/broadcast.tag +++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag @@ -1,4 +1,4 @@ - +
@@ -8,14 +8,27 @@
-

開発者募集中!

-

Misskeyはオープンソースで開発されています。リポジトリはこちら。

+

%i18n:desktop.tags.mk-broadcast-home-widget.fetching%

+

{ + broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title + }

+

%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%

+ 1 } onclick={ next }>%i18n:desktop.tags.mk-broadcast-home-widget.next% >> +
diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/tags/home-widgets/calendar.tag index 9aa4ac632..fded57e07 100644 --- a/src/web/app/desktop/tags/home-widgets/calendar.tag +++ b/src/web/app/desktop/tags/home-widgets/calendar.tag @@ -1,4 +1,4 @@ - +

{ year }年{ month }月

{ day }日

@@ -30,9 +30,15 @@ padding 16px 0 color #777 background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px &[data-special='on-new-years-day'] - border-color #ef95a0 !important + border-color #ef95a0 + + &[data-melt] + background transparent + border none &:after content "" @@ -106,6 +112,12 @@ diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag new file mode 100644 index 000000000..f22a5f76e --- /dev/null +++ b/src/web/app/desktop/tags/home-widgets/channel.tag @@ -0,0 +1,318 @@ + + +

{ + channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' + }

+ +
+

%i18n:desktop.tags.mk-channel-home-widget.get-started%

+ + + +
+ + +

読み込み中

+
+

まだ投稿がありません

+ +
+ + + +
+ + +
+ { post.index }: + { post.user.name } + ID:{ post.user.username } +
+
+ >>{ post.reply.index } + { post.text } +
+ + + { + + +
+
+ + +
+ + + + + + diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/tags/home-widgets/donation.tag index d533e8283..99ded1b5d 100644 --- a/src/web/app/desktop/tags/home-widgets/donation.tag +++ b/src/web/app/desktop/tags/home-widgets/donation.tag @@ -7,7 +7,8 @@ :scope display block background #fff - border-color #ead8bb !important + border solid 1px #ead8bb + border-radius 6px > article padding 20px @@ -28,5 +29,8 @@ color #999 - + diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag index b94e9b04c..257afc4a8 100644 --- a/src/web/app/desktop/tags/home-widgets/mentions.tag +++ b/src/web/app/desktop/tags/home-widgets/mentions.tag @@ -9,6 +9,8 @@ :scope display block background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px > header padding 8px 16px diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag new file mode 100644 index 000000000..52251aa53 --- /dev/null +++ b/src/web/app/desktop/tags/home-widgets/messaging.tag @@ -0,0 +1,52 @@ + + +

%i18n:desktop.tags.mk-messaging-home-widget.title%

+
+ + + +
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag index 54bfb87a1..61c0b4cb5 100644 --- a/src/web/app/desktop/tags/home-widgets/nav.tag +++ b/src/web/app/desktop/tags/home-widgets/nav.tag @@ -1,4 +1,5 @@ -%i18n:desktop.tags.mk-nav-home-widget.about%%i18n:desktop.tags.mk-nav-home-widget.stats%%i18n:desktop.tags.mk-nav-home-widget.status%%i18n:desktop.tags.mk-nav-home-widget.wiki%%i18n:desktop.tags.mk-nav-home-widget.donors%%i18n:desktop.tags.mk-nav-home-widget.repository%%i18n:desktop.tags.mk-nav-home-widget.develop%Follow us on + + + diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag index b1170855a..dadafa660 100644 --- a/src/web/app/desktop/tags/home-widgets/notifications.tag +++ b/src/web/app/desktop/tags/home-widgets/notifications.tag @@ -1,11 +1,15 @@ -

%i18n:desktop.tags.mk-notifications-home-widget.title%

- + +

%i18n:desktop.tags.mk-notifications-home-widget.title%

+ +
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag index d1f29589f..05658c902 100644 --- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag +++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag @@ -1,5 +1,7 @@ - -

%i18n:desktop.tags.mk-photo-stream-home-widget.title%

+ + +

%i18n:desktop.tags.mk-photo-stream-home-widget.title%

+

%i18n:common.loading%

0 }> @@ -11,6 +13,19 @@ :scope display block background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px + + &[data-melt] + background transparent !important + border none !important + + > .stream + padding 0 + + > .img + border solid 4px transparent + border-radius 8px > .title z-index 1 @@ -55,15 +70,21 @@ diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag new file mode 100644 index 000000000..9ca7fecfe --- /dev/null +++ b/src/web/app/desktop/tags/home-widgets/post-form.tag @@ -0,0 +1,103 @@ + + + + +

%i18n:desktop.tags.mk-post-form-home-widget.title%

+
+ + +
+ + +
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag index e6a875211..eb8ba52e8 100644 --- a/src/web/app/desktop/tags/home-widgets/profile.tag +++ b/src/web/app/desktop/tags/home-widgets/profile.tag @@ -1,11 +1,56 @@ - - avatar{ I.name } + + + avatar + { I.name }

@{ I.username }

diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag index e9b740762..fe04ee0e2 100644 --- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag +++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag @@ -1,6 +1,8 @@ -

RSS

- + +

RSS

+ +
@@ -9,6 +11,8 @@ :scope display block background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px > .title margin 0 @@ -62,6 +66,12 @@
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag index bc8f313d5..b37d34736 100644 --- a/src/web/app/desktop/tags/home-widgets/server.tag +++ b/src/web/app/desktop/tags/home-widgets/server.tag @@ -1,17 +1,25 @@ - -

%i18n:desktop.tags.mk-server-home-widget.title%

- + + +

%i18n:desktop.tags.mk-server-home-widget.title%

+ +

%i18n:common.loading%

- - - - - - + + + + + +
@@ -164,7 +187,7 @@ clear both + diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag index 08d96ad71..c751069f7 100644 --- a/src/web/app/desktop/tags/home-widgets/timeline.tag +++ b/src/web/app/desktop/tags/home-widgets/timeline.tag @@ -3,12 +3,18 @@
-

自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。

- +

自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。

+ + + + + + + diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag index 5a535099a..81cea6464 100644 --- a/src/web/app/desktop/tags/home-widgets/tips.tag +++ b/src/web/app/desktop/tags/home-widgets/tips.tag @@ -3,8 +3,6 @@ diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag index f78d7944f..cf563db53 100644 --- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag +++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag @@ -1,6 +1,8 @@ -

%i18n:desktop.tags.mk-user-recommendation-home-widget.title%

- + +

%i18n:desktop.tags.mk-user-recommendation-home-widget.title%

+ +
@@ -17,6 +19,8 @@ :scope display block background #fff + border solid 1px rgba(0, 0, 0, 0.075) + border-radius 6px > .title margin 0 @@ -111,7 +115,11 @@ diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag index ea5307061..2b66b0490 100644 --- a/src/web/app/desktop/tags/home-widgets/version.tag +++ b/src/web/app/desktop/tags/home-widgets/version.tag @@ -1,10 +1,8 @@ -

ver { version } (葵 aoi)

+

ver { _VERSION_ } (葵 aoi)

diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag index 37b2d3cf7..55f36e097 100644 --- a/src/web/app/desktop/tags/home.tag +++ b/src/web/app/desktop/tags/home.tag @@ -1,50 +1,173 @@ - + +
+ 完了 +
+
+

ウィジェットを追加:

+ + +
+
+
+

ゴミ箱

+
+
+
-
-
+
+
+
+
+
-
+
+
+
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.ts similarity index 85% rename from src/web/app/desktop/tags/index.js rename to src/web/app/desktop/tags/index.ts index 37fdfe37e..3ec1d108a 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.ts @@ -12,6 +12,7 @@ require('./drive/nav-folder.tag'); require('./drive/browser-window.tag'); require('./drive/browser.tag'); require('./select-file-from-drive-window.tag'); +require('./select-folder-from-drive-window.tag'); require('./crop-window.tag'); require('./settings.tag'); require('./settings-window.tag'); @@ -38,6 +39,12 @@ require('./home-widgets/recommended-polls.tag'); require('./home-widgets/trends.tag'); require('./home-widgets/activity.tag'); require('./home-widgets/server.tag'); +require('./home-widgets/slideshow.tag'); +require('./home-widgets/channel.tag'); +require('./home-widgets/timemachine.tag'); +require('./home-widgets/post-form.tag'); +require('./home-widgets/access-log.tag'); +require('./home-widgets/messaging.tag'); require('./timeline.tag'); require('./messaging/window.tag'); require('./messaging/room-window.tag'); @@ -45,23 +52,19 @@ require('./following-setuper.tag'); require('./ellipsis-icon.tag'); require('./ui.tag'); require('./home.tag'); -require('./user-header.tag'); -require('./user-profile.tag'); require('./user-timeline.tag'); require('./user.tag'); -require('./user-home.tag'); -require('./user-graphs.tag'); -require('./user-photos.tag'); require('./big-follow-button.tag'); require('./pages/entrance.tag'); -require('./pages/entrance/signin.tag'); -require('./pages/entrance/signup.tag'); require('./pages/home.tag'); +require('./pages/home-customize.tag'); require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); require('./pages/not-found.tag'); require('./pages/selectdrive.tag'); +require('./pages/drive.tag'); +require('./pages/messaging-room.tag'); require('./autocomplete-suggestion.tag'); require('./progress-dialog.tag'); require('./user-preview.tag'); @@ -83,3 +86,5 @@ require('./user-following-window.tag'); require('./user-followers-window.tag'); require('./list-user.tag'); require('./detailed-post-window.tag'); +require('./widgets/calendar.tag'); +require('./widgets/activity.tag'); diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag index 5d8a4303a..1c6ff7c4b 100644 --- a/src/web/app/desktop/tags/messaging/room-window.tag +++ b/src/web/app/desktop/tags/messaging/room-window.tag @@ -1,5 +1,5 @@ - + メッセージ: { parent.user.name } @@ -21,6 +21,8 @@ + diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag index 7ad19c073..02aeb922f 100644 --- a/src/web/app/desktop/tags/pages/entrance.tag +++ b/src/web/app/desktop/tags/pages/entrance.tag @@ -1,16 +1,25 @@
- Misskey - - -
- - +
+

どこにいても、ここにあります

+

ようこそ! MisskeyはTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。

+

これまでに{ stats.posts_count }投稿されました

+
+
+ + +
+ + +
- +
+ + +
+ + + +
+

+

{ user ? user.name : 'アカウント' }

+

+ +
+ Twitterでサインイン +
or
+ Misskeyについて + + +
+ + + + + + diff --git a/src/web/app/desktop/tags/pages/entrance/signin.tag b/src/web/app/desktop/tags/pages/entrance/signin.tag deleted file mode 100644 index 6caa747c1..000000000 --- a/src/web/app/desktop/tags/pages/entrance/signin.tag +++ /dev/null @@ -1,134 +0,0 @@ - -
-

-

{ user ? user.name : 'アカウント' }

-

- -
-
or
- Misskeyについて - - -
diff --git a/src/web/app/desktop/tags/pages/entrance/signup.tag b/src/web/app/desktop/tags/pages/entrance/signup.tag deleted file mode 100644 index 0722d82a6..000000000 --- a/src/web/app/desktop/tags/pages/entrance/signup.tag +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/tags/pages/home-customize.tag new file mode 100644 index 000000000..457b8390e --- /dev/null +++ b/src/web/app/desktop/tags/pages/home-customize.tag @@ -0,0 +1,12 @@ + + + + + diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag index e8ba4023d..3c8f4ec57 100644 --- a/src/web/app/desktop/tags/pages/home.tag +++ b/src/web/app/desktop/tags/pages/home.tag @@ -12,10 +12,12 @@ this.mixin('i'); this.mixin('api'); + this.mixin('stream'); + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); this.unreadCount = 0; - this.page = this.opts.mode || 'timeline'; this.on('mount', () => { @@ -24,12 +26,14 @@ }); document.title = 'Misskey'; Progress.start(); - this.stream.on('post', this.onStreamPost); + + this.connection.on('post', this.onStreamPost); document.addEventListener('visibilitychange', this.windowOnVisibilitychange, false); }); this.on('unmount', () => { - this.stream.off('post', this.onStreamPost); + this.connection.off('post', this.onStreamPost); + this.stream.dispose(this.connectionId); document.removeEventListener('visibilitychange', this.windowOnVisibilitychange); }); diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/tags/pages/messaging-room.tag new file mode 100644 index 000000000..3c21b9750 --- /dev/null +++ b/src/web/app/desktop/tags/pages/messaging-room.tag @@ -0,0 +1,37 @@ + + + + + + diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag index f270b43ac..4a9672c1e 100644 --- a/src/web/app/desktop/tags/pages/post.tag +++ b/src/web/app/desktop/tags/pages/post.tag @@ -28,6 +28,7 @@ > mk-post-detail margin 0 auto + width 640px + diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag index eabddfb43..4c16f9eaa 100644 --- a/src/web/app/desktop/tags/settings.tag +++ b/src/web/app/desktop/tags/settings.tag @@ -38,6 +38,7 @@

デザイン

+ ホームをカスタマイズ
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag index c75ae2911..86269fdbe 100644 --- a/src/web/app/desktop/tags/sub-post-content.tag +++ b/src/web/app/desktop/tags/sub-post-content.tag @@ -45,7 +45,7 @@ const tokens = this.post.ast; this.refs.text.innerHTML = compile(tokens, false); - this.refs.text.children.forEach(e => { + Array.from(this.refs.text.children).forEach(e => { if (e.tagName == 'MK-URL') riot.mount(e); }); } diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag index 44f3d5d8e..13651dfa5 100644 --- a/src/web/app/desktop/tags/timeline.tag +++ b/src/web/app/desktop/tags/timeline.tag @@ -112,7 +112,7 @@
-

{ p.channel.title }:

+

{ p.channel.title }:

@@ -430,9 +430,12 @@ this.mixin('i'); this.mixin('api'); - this.mixin('stream'); this.mixin('user-preview'); + this.mixin('stream'); + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); + this.isDetailOpened = false; this.set = post => { @@ -468,21 +471,21 @@ this.capture = withHandler => { if (this.SIGNIN) { - this.stream.send({ + this.connection.send({ type: 'capture', id: this.post.id }); - if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated); + if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated); } }; this.decapture = withHandler => { if (this.SIGNIN) { - this.stream.send({ + this.connection.send({ type: 'decapture', id: this.post.id }); - if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated); + if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated); } }; @@ -490,7 +493,7 @@ this.capture(true); if (this.SIGNIN) { - this.stream.on('_connected_', this.onStreamConnected); + this.connection.on('_connected_', this.onStreamConnected); } if (this.p.text) { @@ -498,7 +501,7 @@ this.refs.text.innerHTML = this.refs.text.innerHTML.replace('

', compile(tokens)); - this.refs.text.children.forEach(e => { + Array.from(this.refs.text.children).forEach(e => { if (e.tagName == 'MK-URL') riot.mount(e); }); @@ -515,7 +518,8 @@ this.on('unmount', () => { this.decapture(true); - this.stream.off('_connected_', this.onStreamConnected); + this.connection.off('_connected_', this.onStreamConnected); + this.stream.dispose(this.connectionId); }); this.reply = () => { diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index 3123c34f4..047964fab 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -37,7 +37,7 @@ - +
@@ -75,8 +75,7 @@ width 100% height 48px backdrop-filter blur(12px) - //background-color rgba(255, 255, 255, 0.75) - background #1d2429 + background #f7f7f7 &:after content "" @@ -138,23 +137,28 @@ > input user-select text cursor auto - margin 0 + margin 8px 0 0 0 padding 6px 18px width 14em - height 48px + height 32px font-size 1em - line-height calc(48px - 12px) - background transparent + background rgba(0, 0, 0, 0.05) outline none //border solid 1px #ddd border none - border-radius 0 + border-radius 16px transition color 0.5s ease, border 0.5s ease font-family FontAwesome, sans-serif &::-webkit-input-placeholder color #9eaba8 + &:hover + background rgba(0, 0, 0, 0.08) + + &:focus + box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important + - diff --git a/src/web/app/desktop/tags/user-header.tag b/src/web/app/desktop/tags/user-header.tag deleted file mode 100644 index ea7ea6bb3..000000000 --- a/src/web/app/desktop/tags/user-header.tag +++ /dev/null @@ -1,147 +0,0 @@ - - avatar -
-

{ user.name }

-

@{ user.username }

-

{ user.profile.location }

-
- - - -
diff --git a/src/web/app/desktop/tags/user-home.tag b/src/web/app/desktop/tags/user-home.tag deleted file mode 100644 index a879db5bb..000000000 --- a/src/web/app/desktop/tags/user-home.tag +++ /dev/null @@ -1,46 +0,0 @@ - -
- - -
-
- -
- - -
diff --git a/src/web/app/desktop/tags/user-photos.tag b/src/web/app/desktop/tags/user-photos.tag deleted file mode 100644 index dce1e50ad..000000000 --- a/src/web/app/desktop/tags/user-photos.tag +++ /dev/null @@ -1,91 +0,0 @@ - -

フォト

-

読み込んでいます

-
0 }> - -
-
-
-

写真はありません

- - -
diff --git a/src/web/app/desktop/tags/user-profile.tag b/src/web/app/desktop/tags/user-profile.tag deleted file mode 100644 index 7472a4780..000000000 --- a/src/web/app/desktop/tags/user-profile.tag +++ /dev/null @@ -1,102 +0,0 @@ - -
- -

フォローされています

-
-
{ user.description }
-
-

{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)

-
- -
-

{ user.posts_count }ポスト

-

{ user.following_count }人をフォロー

-

{ user.followers_count }人のフォロワー

-
- - -
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag index 08ab47b16..5df13c436 100644 --- a/src/web/app/desktop/tags/user-timeline.tag +++ b/src/web/app/desktop/tags/user-timeline.tag @@ -91,6 +91,7 @@ this.fetch = cb => { this.api('users/posts', { user_id: this.user.id, + max_date: this.date ? this.date.getTime() : undefined, with_replies: this.mode == 'with-replies' }).then(posts => { this.update({ @@ -132,5 +133,13 @@ }); this.fetch(); }; + + this.warp = date => { + this.update({ + date: date + }); + + this.fetch(); + }; diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag index db4fd7cc7..5ec6ac762 100644 --- a/src/web/app/desktop/tags/user.tag +++ b/src/web/app/desktop/tags/user.tag @@ -3,33 +3,18 @@
-
- - -
+ +
+ + + +
+
+ avatar +
+

{ user.name }

+

@{ user.username }

+

{ user.profile.location }

+
+ +
+ + +
+ + +
+ +

フォローされています

+
+
{ user.description }
+
+

{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)

+
+ +
+

{ user.posts_count }ポスト

+

{ user.following_count }人をフォロー

+

{ user.followers_count }人のフォロワー

+
+ + +
+ + +

%i18n:desktop.tags.mk-user.photos.title%

+

%i18n:desktop.tags.mk-user.photos.loading%

+
0 }> + +
+
+
+

%i18n:desktop.tags.mk-user.photos.no-photos%

+ + +
+ + +

%i18n:desktop.tags.mk-user.frequently-replied-users.title%

+

%i18n:desktop.tags.mk-user.frequently-replied-users.loading%

+
+ + + +
+ { _user.name } +

@{ _user.username }

+
+ +
+

%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%

+ + +
+ + +

%i18n:desktop.tags.mk-user.followers-you-know.title%

+

%i18n:desktop.tags.mk-user.followers-you-know.loading%

+
0 }> + + { + +
+

%i18n:desktop.tags.mk-user.followers-you-know.no-users%

+ + +
+ + +
+
+ + + +

%i18n:desktop.tags.mk-user.last-used-at%:

+
+
+
+ + +
+
+
+ + + + +
+
+ + +
+ + +
+
+

投稿

+ +
+
+
+
+

フォロー/フォロワー

+ +
+
+
+
+

いいね

+ +
+
+ + +
+ + + + + + + + + +

直近1年間分の統計です。一番右が現在で、一番左が1年前です。青は通常の投稿、赤は返信、緑はRepostをそれぞれ表しています。

+

+ だいたい*1日に{ averageOfAllTypePostsEachDays }回投稿(返信、Repost含む)しています。
+ だいたい*1日に{ averageOfPostsEachDays }回投稿(通常の)しています。
+ だいたい*1日に{ averageOfRepliesEachDays }回返信しています。
+ だいたい*1日に{ averageOfRepostsEachDays }回Repostしています。
+

+

* 中央値

+ + + +
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag new file mode 100644 index 000000000..baf385fe9 --- /dev/null +++ b/src/web/app/desktop/tags/widgets/activity.tag @@ -0,0 +1,246 @@ + + +

%i18n:desktop.tags.mk-activity-widget.title%

+ +
+

%i18n:common.loading%

+ + + + +
+ + + + + { date.year }/{ date.month }/{ date.day }
Post: { posts }, Reply: { replies }, Repost: { reposts }
+
+ + +
+ + +
+ + + + Black ... Total
Blue ... Posts
Red ... Replies
Green ... Reposts
+ + + + +
+ + +
+ diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag new file mode 100644 index 000000000..5f00d5cf2 --- /dev/null +++ b/src/web/app/desktop/tags/widgets/calendar.tag @@ -0,0 +1,241 @@ + + + +

{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }

+ +
+ +
+
{ weekdayText[i] }
+
+
{ i + 1 }
+
+ + +
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag index aefb6499b..256cfb790 100644 --- a/src/web/app/desktop/tags/window.tag +++ b/src/web/app/desktop/tags/window.tag @@ -4,7 +4,10 @@

- +
+ + +
@@ -117,8 +120,12 @@ box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2) > header + $header-height = 40px + z-index 128 + height $header-height overflow hidden + white-space nowrap cursor move background #fff border-radius 6px 6px 0 0 @@ -130,39 +137,45 @@ > h1 pointer-events none display block - margin 0 - height 40px + margin 0 auto + overflow hidden + height $header-height + text-overflow ellipsis text-align center font-size 1em - line-height 40px + line-height $header-height font-weight normal color #666 - > .close - cursor pointer - display block + > div:last-child position absolute top 0 right 0 + display block z-index 1 - margin 0 - padding 0 - font-size 1.2em - color rgba(#000, 0.4) - border none - outline none - background transparent - &:hover - color rgba(#000, 0.6) - - &:active - color darken(#000, 30%) - - > i + > * + display inline-block + margin 0 padding 0 - width 40px - line-height 40px + cursor pointer + font-size 1.2em + color rgba(#000, 0.4) + border none + outline none + background transparent + + &:hover + color rgba(#000, 0.6) + + &:active + color darken(#000, 30%) + + > i + padding 0 + width $header-height + line-height $header-height + text-align center > .content height 100% @@ -181,6 +194,7 @@ this.isModal = this.opts.isModal != null ? this.opts.isModal : false; this.canClose = this.opts.canClose != null ? this.opts.canClose : true; + this.popoutUrl = this.opts.popout; this.isFlexible = this.opts.height == null; this.canResize = !this.isFlexible; @@ -247,6 +261,22 @@ }, 300); }; + this.popout = () => { + const position = this.refs.main.getBoundingClientRect(); + + const width = parseInt(getComputedStyle(this.refs.main, '').width, 10); + const height = parseInt(getComputedStyle(this.refs.main, '').height, 10); + const x = window.screenX + position.left; + const y = window.screenY + position.top; + + const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl; + + window.open(url, url, + `height=${height},width=${width},left=${x},top=${y}`); + + this.close(); + }; + this.close = () => { this.trigger('closing'); diff --git a/src/web/app/dev/router.js b/src/web/app/dev/router.ts similarity index 91% rename from src/web/app/dev/router.js rename to src/web/app/dev/router.ts index 7fde30fa5..fcd2b1f76 100644 --- a/src/web/app/dev/router.js +++ b/src/web/app/dev/router.ts @@ -1,8 +1,8 @@ import * as riot from 'riot'; -const route = require('page'); +import * as route from 'page'; let page = null; -export default me => { +export default () => { route('/', index); route('/apps', apps); route('/app/new', newApp); @@ -32,7 +32,7 @@ export default me => { } // EXEC - route(); + (route as any)(); }; function mount(content) { diff --git a/src/web/app/dev/script.js b/src/web/app/dev/script.ts similarity index 87% rename from src/web/app/dev/script.js rename to src/web/app/dev/script.ts index 39d7fc891..b115c5be4 100644 --- a/src/web/app/dev/script.js +++ b/src/web/app/dev/script.ts @@ -12,7 +12,7 @@ import route from './router'; /** * init */ -init(me => { +init(() => { // Start routing - route(me); + route(); }); diff --git a/src/web/app/dev/tags/index.js b/src/web/app/dev/tags/index.ts similarity index 100% rename from src/web/app/dev/tags/index.js rename to src/web/app/dev/tags/index.ts diff --git a/src/web/app/init.js b/src/web/app/init.js deleted file mode 100644 index 5a6899ed4..000000000 --- a/src/web/app/init.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * App initializer - */ - -'use strict'; - -import * as riot from 'riot'; -import api from './common/scripts/api'; -import signout from './common/scripts/signout'; -import checkForUpdate from './common/scripts/check-for-update'; -import Connection from './common/scripts/home-stream'; -import Progress from './common/scripts/loading'; -import mixin from './common/mixins'; -import generateDefaultUserdata from './common/scripts/generate-default-userdata'; -import CONFIG from './common/scripts/config'; -require('./common/tags'); - -/** - * APP ENTRY POINT! - */ - -console.info(`Misskey v${VERSION} (葵 aoi)`); - -{ // Set lang attr - const html = document.documentElement; - html.setAttribute('lang', LANG); -} - -{ // Set description meta tag - const head = document.getElementsByTagName('head')[0]; - const meta = document.createElement('meta'); - meta.setAttribute('name', 'description'); - meta.setAttribute('content', '%i18n:common.misskey%'); - head.appendChild(meta); -} - -document.domain = CONFIG.host; - -// Set global configuration -riot.mixin({ CONFIG }); - -// ↓ NodeList、HTMLCollection、FileList、DataTransferItemListで forEach を使えるようにする -if (NodeList.prototype.forEach === undefined) { - NodeList.prototype.forEach = Array.prototype.forEach; -} -if (HTMLCollection.prototype.forEach === undefined) { - HTMLCollection.prototype.forEach = Array.prototype.forEach; -} -if (FileList.prototype.forEach === undefined) { - FileList.prototype.forEach = Array.prototype.forEach; -} -if (window.DataTransferItemList && DataTransferItemList.prototype.forEach === undefined) { - DataTransferItemList.prototype.forEach = Array.prototype.forEach; -} - -// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする -try { - localStorage.setItem('kyoppie', 'yuppie'); -} catch (e) { - Storage.prototype.setItem = () => { }; // noop -} - -// クライアントを更新すべきならする -if (localStorage.getItem('should-refresh') == 'true') { - localStorage.removeItem('should-refresh'); - location.reload(true); -} - -// 更新チェック -setTimeout(checkForUpdate, 3000); - -// ユーザーをフェッチしてコールバックする -export default callback => { - // Get cached account data - let cachedMe = JSON.parse(localStorage.getItem('me')); - - if (cachedMe) { - fetched(cachedMe); - - // 後から新鮮なデータをフェッチ - fetchme(cachedMe.token, freshData => { - Object.assign(cachedMe, freshData); - cachedMe.trigger('updated'); - }); - } else { - // Get token from cookie - const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; - - fetchme(i, fetched); - } - - // フェッチが完了したとき - function 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)); - }); - } - - // Init home stream connection - const stream = me ? new Connection(me) : null; - - // ミックスイン初期化 - mixin(me, stream); - - // ローディング画面クリア - const ini = document.getElementById('ini'); - ini.parentNode.removeChild(ini); - - // アプリ基底要素マウント - const app = document.createElement('div'); - app.setAttribute('id', 'app'); - document.body.appendChild(app); - - try { - callback(me, stream); - } catch (e) { - panic(e); - } - } -}; - -// ユーザーをフェッチしてコールバックする -function fetchme(token, cb) { - let me = null; - - // Return when not signed in - if (token == null) { - return done(); - } - - // Fetch user - fetch(`${CONFIG.apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token - }) - }).then(res => { // When success - // When failed to authenticate user - if (res.status !== 200) { - return signout(); - } - - res.json().then(i => { - me = i; - me.token = token; - - // initialize it if user data is empty - me.data ? done() : init(); - }); - }, () => { // When failure - // Render the error screen - document.body.innerHTML = ''; - riot.mount('*'); - Progress.done(); - }); - - function done() { - if (cb) cb(me); - } - - // Initialize user data - function init() { - const data = generateDefaultUserdata(); - api(token, 'i/appdata/set', { - data - }).then(() => { - me.data = data; - done(); - }); - } -} - -// BSoD -function panic(e) { - console.error(e); - - // Display blue screen - document.documentElement.style.background = '#1269e2'; - document.body.innerHTML = - '
' - + '

:( 致命的な問題が発生しました。

' - + '

お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。

' - + '
' - + `

エラーコード: ${e.toString()}

` - + `

ブラウザ バージョン: ${navigator.userAgent}

` - + `

クライアント バージョン: ${VERSION}

` - + '
' - + '

問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。

' - + '

Thank you for using Misskey.

' - + '
'; - - // TODO: Report the bug -} diff --git a/src/web/app/init.ts b/src/web/app/init.ts new file mode 100644 index 000000000..79be1d368 --- /dev/null +++ b/src/web/app/init.ts @@ -0,0 +1,105 @@ +/** + * App initializer + */ + +declare const _VERSION_: string; +declare const _LANG_: string; +declare const _HOST_: string; +declare const __CONSTS__: any; + +import * as riot from 'riot'; +import checkForUpdate from './common/scripts/check-for-update'; +import mixin from './common/mixins'; +import MiOS from './common/mios'; +require('./common/tags'); + +/** + * APP ENTRY POINT! + */ + +console.info(`Misskey v${_VERSION_} (葵 aoi)`); + +if (_HOST_ != 'localhost') { + document.domain = _HOST_; +} + +{ // Set lang attr + const html = document.documentElement; + html.setAttribute('lang', _LANG_); +} + +{ // Set description meta tag + const head = document.getElementsByTagName('head')[0]; + const meta = document.createElement('meta'); + meta.setAttribute('name', 'description'); + meta.setAttribute('content', '%i18n:common.misskey%'); + head.appendChild(meta); +} + +// Set global configuration +(riot as any).mixin(__CONSTS__); + +// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする +try { + localStorage.setItem('kyoppie', 'yuppie'); +} catch (e) { + Storage.prototype.setItem = () => { }; // noop +} + +// クライアントを更新すべきならする +if (localStorage.getItem('should-refresh') == 'true') { + localStorage.removeItem('should-refresh'); + location.reload(true); +} + +// MiOSを初期化してコールバックする +export default (callback, sw = false) => { + const mios = new MiOS(sw); + + mios.init(() => { + // ミックスイン初期化 + mixin(mios); + + // ローディング画面クリア + const ini = document.getElementById('ini'); + ini.parentNode.removeChild(ini); + + // アプリ基底要素マウント + const app = document.createElement('div'); + app.setAttribute('id', 'app'); + document.body.appendChild(app); + + try { + callback(mios); + } catch (e) { + panic(e); + } + + // 更新チェック + setTimeout(() => { + checkForUpdate(mios); + }, 3000); + }); +}; + +// BSoD +function panic(e) { + console.error(e); + + // Display blue screen + document.documentElement.style.background = '#1269e2'; + document.body.innerHTML = + '
' + + '

:( 致命的な問題が発生しました。

' + + '

お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。

' + + '
' + + `

エラーコード: ${e.toString()}

` + + `

ブラウザ バージョン: ${navigator.userAgent}

` + + `

クライアント バージョン: ${_VERSION_}

` + + '
' + + '

問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。

' + + '

Thank you for using Misskey.

' + + '
'; + + // TODO: Report the bug +} diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.ts similarity index 95% rename from src/web/app/mobile/router.js rename to src/web/app/mobile/router.ts index 01eb3c814..0358d10e9 100644 --- a/src/web/app/mobile/router.js +++ b/src/web/app/mobile/router.ts @@ -3,10 +3,11 @@ */ import * as riot from 'riot'; -const route = require('page'); +import * as route from 'page'; +import MiOS from '../common/mios'; let page = null; -export default me => { +export default (mios: MiOS) => { route('/', index); route('/selectdrive', selectDrive); route('/i/notifications', notifications); @@ -32,7 +33,7 @@ export default me => { route('*', notFound); function index() { - me ? home() : entrance(); + mios.isSignedin ? home() : entrance(); } function home() { @@ -131,12 +132,12 @@ export default me => { mount(document.createElement('mk-not-found')); } - riot.mixin('page', { + (riot as any).mixin('page', { page: route }); // EXEC - route(); + (route as any)(); }; function mount(content) { diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.ts similarity index 77% rename from src/web/app/mobile/script.js rename to src/web/app/mobile/script.ts index 503e0fd67..4dfff8f72 100644 --- a/src/web/app/mobile/script.js +++ b/src/web/app/mobile/script.ts @@ -8,14 +8,15 @@ import './style.styl'; require('./tags'); import init from '../init'; import route from './router'; +import MiOS from '../common/mios'; /** * init */ -init(me => { +init((mios: MiOS) => { // http://qiita.com/junya/items/3ff380878f26ca447f85 document.body.setAttribute('ontouchstart', ''); // Start routing - route(me); -}); + route(mios); +}, true); diff --git a/src/web/app/mobile/scripts/open-post-form.js b/src/web/app/mobile/scripts/open-post-form.ts similarity index 100% rename from src/web/app/mobile/scripts/open-post-form.js rename to src/web/app/mobile/scripts/open-post-form.ts diff --git a/src/web/app/mobile/scripts/ui-event.js b/src/web/app/mobile/scripts/ui-event.ts similarity index 100% rename from src/web/app/mobile/scripts/ui-event.js rename to src/web/app/mobile/scripts/ui-event.ts diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag index 6929c50ab..2c36c43ac 100644 --- a/src/web/app/mobile/tags/drive.tag +++ b/src/web/app/mobile/tags/drive.tag @@ -172,7 +172,10 @@ diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag index e6129652b..2cec4f329 100644 --- a/src/web/app/mobile/tags/drive/file-viewer.tag +++ b/src/web/app/mobile/tags/drive/file-viewer.tag @@ -2,7 +2,7 @@
{ -
+
{ file.properties.width } × @@ -44,7 +44,7 @@

%i18n:mobile.tags.mk-drive-file-viewer.hash%

- { file.hash } + { file.md5 }